You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
394 lines
14 KiB
394 lines
14 KiB
//===--- TestSupport.cpp - Clang-based refactoring tool -------------------===//
|
|
//
|
|
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
|
|
// See https://llvm.org/LICENSE.txt for license information.
|
|
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
|
|
//
|
|
//===----------------------------------------------------------------------===//
|
|
///
|
|
/// \file
|
|
/// This file implements routines that provide refactoring testing
|
|
/// utilities.
|
|
///
|
|
//===----------------------------------------------------------------------===//
|
|
|
|
#include "TestSupport.h"
|
|
#include "clang/Basic/DiagnosticError.h"
|
|
#include "clang/Basic/FileManager.h"
|
|
#include "clang/Basic/SourceManager.h"
|
|
#include "clang/Lex/Lexer.h"
|
|
#include "llvm/ADT/STLExtras.h"
|
|
#include "llvm/Support/Error.h"
|
|
#include "llvm/Support/ErrorOr.h"
|
|
#include "llvm/Support/LineIterator.h"
|
|
#include "llvm/Support/MemoryBuffer.h"
|
|
#include "llvm/Support/Regex.h"
|
|
#include "llvm/Support/raw_ostream.h"
|
|
|
|
using namespace llvm;
|
|
|
|
namespace clang {
|
|
namespace refactor {
|
|
|
|
void TestSelectionRangesInFile::dump(raw_ostream &OS) const {
|
|
for (const auto &Group : GroupedRanges) {
|
|
OS << "Test selection group '" << Group.Name << "':\n";
|
|
for (const auto &Range : Group.Ranges) {
|
|
OS << " " << Range.Begin << "-" << Range.End << "\n";
|
|
}
|
|
}
|
|
}
|
|
|
|
bool TestSelectionRangesInFile::foreachRange(
|
|
const SourceManager &SM,
|
|
llvm::function_ref<void(SourceRange)> Callback) const {
|
|
auto FE = SM.getFileManager().getFile(Filename);
|
|
FileID FID = FE ? SM.translateFile(*FE) : FileID();
|
|
if (!FE || FID.isInvalid()) {
|
|
llvm::errs() << "error: -selection=test:" << Filename
|
|
<< " : given file is not in the target TU";
|
|
return true;
|
|
}
|
|
SourceLocation FileLoc = SM.getLocForStartOfFile(FID);
|
|
for (const auto &Group : GroupedRanges) {
|
|
for (const TestSelectionRange &Range : Group.Ranges) {
|
|
// Translate the offset pair to a true source range.
|
|
SourceLocation Start =
|
|
SM.getMacroArgExpandedLocation(FileLoc.getLocWithOffset(Range.Begin));
|
|
SourceLocation End =
|
|
SM.getMacroArgExpandedLocation(FileLoc.getLocWithOffset(Range.End));
|
|
assert(Start.isValid() && End.isValid() && "unexpected invalid range");
|
|
Callback(SourceRange(Start, End));
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
namespace {
|
|
|
|
void dumpChanges(const tooling::AtomicChanges &Changes, raw_ostream &OS) {
|
|
for (const auto &Change : Changes)
|
|
OS << const_cast<tooling::AtomicChange &>(Change).toYAMLString() << "\n";
|
|
}
|
|
|
|
bool areChangesSame(const tooling::AtomicChanges &LHS,
|
|
const tooling::AtomicChanges &RHS) {
|
|
if (LHS.size() != RHS.size())
|
|
return false;
|
|
for (auto I : llvm::zip(LHS, RHS)) {
|
|
if (!(std::get<0>(I) == std::get<1>(I)))
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool printRewrittenSources(const tooling::AtomicChanges &Changes,
|
|
raw_ostream &OS) {
|
|
std::set<std::string> Files;
|
|
for (const auto &Change : Changes)
|
|
Files.insert(Change.getFilePath());
|
|
tooling::ApplyChangesSpec Spec;
|
|
Spec.Cleanup = false;
|
|
for (const auto &File : Files) {
|
|
llvm::ErrorOr<std::unique_ptr<llvm::MemoryBuffer>> BufferErr =
|
|
llvm::MemoryBuffer::getFile(File);
|
|
if (!BufferErr) {
|
|
llvm::errs() << "failed to open" << File << "\n";
|
|
return true;
|
|
}
|
|
auto Result = tooling::applyAtomicChanges(File, (*BufferErr)->getBuffer(),
|
|
Changes, Spec);
|
|
if (!Result) {
|
|
llvm::errs() << toString(Result.takeError());
|
|
return true;
|
|
}
|
|
OS << *Result;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
class TestRefactoringResultConsumer final
|
|
: public ClangRefactorToolConsumerInterface {
|
|
public:
|
|
TestRefactoringResultConsumer(const TestSelectionRangesInFile &TestRanges)
|
|
: TestRanges(TestRanges) {
|
|
Results.push_back({});
|
|
}
|
|
|
|
~TestRefactoringResultConsumer() {
|
|
// Ensure all results are checked.
|
|
for (auto &Group : Results) {
|
|
for (auto &Result : Group) {
|
|
if (!Result) {
|
|
(void)llvm::toString(Result.takeError());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void handleError(llvm::Error Err) override { handleResult(std::move(Err)); }
|
|
|
|
void handle(tooling::AtomicChanges Changes) override {
|
|
handleResult(std::move(Changes));
|
|
}
|
|
|
|
void handle(tooling::SymbolOccurrences Occurrences) override {
|
|
tooling::RefactoringResultConsumer::handle(std::move(Occurrences));
|
|
}
|
|
|
|
private:
|
|
bool handleAllResults();
|
|
|
|
void handleResult(Expected<tooling::AtomicChanges> Result) {
|
|
Results.back().push_back(std::move(Result));
|
|
size_t GroupIndex = Results.size() - 1;
|
|
if (Results.back().size() >=
|
|
TestRanges.GroupedRanges[GroupIndex].Ranges.size()) {
|
|
++GroupIndex;
|
|
if (GroupIndex >= TestRanges.GroupedRanges.size()) {
|
|
if (handleAllResults())
|
|
exit(1); // error has occurred.
|
|
return;
|
|
}
|
|
Results.push_back({});
|
|
}
|
|
}
|
|
|
|
const TestSelectionRangesInFile &TestRanges;
|
|
std::vector<std::vector<Expected<tooling::AtomicChanges>>> Results;
|
|
};
|
|
|
|
std::pair<unsigned, unsigned> getLineColumn(StringRef Filename,
|
|
unsigned Offset) {
|
|
ErrorOr<std::unique_ptr<MemoryBuffer>> ErrOrFile =
|
|
MemoryBuffer::getFile(Filename);
|
|
if (!ErrOrFile)
|
|
return {0, 0};
|
|
StringRef Source = ErrOrFile.get()->getBuffer();
|
|
Source = Source.take_front(Offset);
|
|
size_t LastLine = Source.find_last_of("\r\n");
|
|
return {Source.count('\n') + 1,
|
|
(LastLine == StringRef::npos ? Offset : Offset - LastLine) + 1};
|
|
}
|
|
|
|
} // end anonymous namespace
|
|
|
|
bool TestRefactoringResultConsumer::handleAllResults() {
|
|
bool Failed = false;
|
|
for (auto &Group : llvm::enumerate(Results)) {
|
|
// All ranges in the group must produce the same result.
|
|
Optional<tooling::AtomicChanges> CanonicalResult;
|
|
Optional<std::string> CanonicalErrorMessage;
|
|
for (auto &I : llvm::enumerate(Group.value())) {
|
|
Expected<tooling::AtomicChanges> &Result = I.value();
|
|
std::string ErrorMessage;
|
|
bool HasResult = !!Result;
|
|
if (!HasResult) {
|
|
handleAllErrors(
|
|
Result.takeError(),
|
|
[&](StringError &Err) { ErrorMessage = Err.getMessage(); },
|
|
[&](DiagnosticError &Err) {
|
|
const PartialDiagnosticAt &Diag = Err.getDiagnostic();
|
|
llvm::SmallString<100> DiagText;
|
|
Diag.second.EmitToString(getDiags(), DiagText);
|
|
ErrorMessage = std::string(DiagText);
|
|
});
|
|
}
|
|
if (!CanonicalResult && !CanonicalErrorMessage) {
|
|
if (HasResult)
|
|
CanonicalResult = std::move(*Result);
|
|
else
|
|
CanonicalErrorMessage = std::move(ErrorMessage);
|
|
continue;
|
|
}
|
|
|
|
// Verify that this result corresponds to the canonical result.
|
|
if (CanonicalErrorMessage) {
|
|
// The error messages must match.
|
|
if (!HasResult && ErrorMessage == *CanonicalErrorMessage)
|
|
continue;
|
|
} else {
|
|
assert(CanonicalResult && "missing canonical result");
|
|
// The results must match.
|
|
if (HasResult && areChangesSame(*Result, *CanonicalResult))
|
|
continue;
|
|
}
|
|
Failed = true;
|
|
// Report the mismatch.
|
|
std::pair<unsigned, unsigned> LineColumn = getLineColumn(
|
|
TestRanges.Filename,
|
|
TestRanges.GroupedRanges[Group.index()].Ranges[I.index()].Begin);
|
|
llvm::errs()
|
|
<< "error: unexpected refactoring result for range starting at "
|
|
<< LineColumn.first << ':' << LineColumn.second << " in group '"
|
|
<< TestRanges.GroupedRanges[Group.index()].Name << "':\n ";
|
|
if (HasResult)
|
|
llvm::errs() << "valid result";
|
|
else
|
|
llvm::errs() << "error '" << ErrorMessage << "'";
|
|
llvm::errs() << " does not match initial ";
|
|
if (CanonicalErrorMessage)
|
|
llvm::errs() << "error '" << *CanonicalErrorMessage << "'\n";
|
|
else
|
|
llvm::errs() << "valid result\n";
|
|
if (HasResult && !CanonicalErrorMessage) {
|
|
llvm::errs() << " Expected to Produce:\n";
|
|
dumpChanges(*CanonicalResult, llvm::errs());
|
|
llvm::errs() << " Produced:\n";
|
|
dumpChanges(*Result, llvm::errs());
|
|
}
|
|
}
|
|
|
|
// Dump the results:
|
|
const auto &TestGroup = TestRanges.GroupedRanges[Group.index()];
|
|
if (!CanonicalResult) {
|
|
llvm::outs() << TestGroup.Ranges.size() << " '" << TestGroup.Name
|
|
<< "' results:\n";
|
|
llvm::outs() << *CanonicalErrorMessage << "\n";
|
|
} else {
|
|
llvm::outs() << TestGroup.Ranges.size() << " '" << TestGroup.Name
|
|
<< "' results:\n";
|
|
if (printRewrittenSources(*CanonicalResult, llvm::outs()))
|
|
return true;
|
|
}
|
|
}
|
|
return Failed;
|
|
}
|
|
|
|
std::unique_ptr<ClangRefactorToolConsumerInterface>
|
|
TestSelectionRangesInFile::createConsumer() const {
|
|
return std::make_unique<TestRefactoringResultConsumer>(*this);
|
|
}
|
|
|
|
/// Adds the \p ColumnOffset to file offset \p Offset, without going past a
|
|
/// newline.
|
|
static unsigned addColumnOffset(StringRef Source, unsigned Offset,
|
|
unsigned ColumnOffset) {
|
|
if (!ColumnOffset)
|
|
return Offset;
|
|
StringRef Substr = Source.drop_front(Offset).take_front(ColumnOffset);
|
|
size_t NewlinePos = Substr.find_first_of("\r\n");
|
|
return Offset +
|
|
(NewlinePos == StringRef::npos ? ColumnOffset : (unsigned)NewlinePos);
|
|
}
|
|
|
|
static unsigned addEndLineOffsetAndEndColumn(StringRef Source, unsigned Offset,
|
|
unsigned LineNumberOffset,
|
|
unsigned Column) {
|
|
StringRef Line = Source.drop_front(Offset);
|
|
unsigned LineOffset = 0;
|
|
for (; LineNumberOffset != 0; --LineNumberOffset) {
|
|
size_t NewlinePos = Line.find_first_of("\r\n");
|
|
// Line offset goes out of bounds.
|
|
if (NewlinePos == StringRef::npos)
|
|
break;
|
|
LineOffset += NewlinePos + 1;
|
|
Line = Line.drop_front(NewlinePos + 1);
|
|
}
|
|
// Source now points to the line at +lineOffset;
|
|
size_t LineStart = Source.find_last_of("\r\n", /*From=*/Offset + LineOffset);
|
|
return addColumnOffset(
|
|
Source, LineStart == StringRef::npos ? 0 : LineStart + 1, Column - 1);
|
|
}
|
|
|
|
Optional<TestSelectionRangesInFile>
|
|
findTestSelectionRanges(StringRef Filename) {
|
|
ErrorOr<std::unique_ptr<MemoryBuffer>> ErrOrFile =
|
|
MemoryBuffer::getFile(Filename);
|
|
if (!ErrOrFile) {
|
|
llvm::errs() << "error: -selection=test:" << Filename
|
|
<< " : could not open the given file";
|
|
return None;
|
|
}
|
|
StringRef Source = ErrOrFile.get()->getBuffer();
|
|
|
|
// See the doc comment for this function for the explanation of this
|
|
// syntax.
|
|
static const Regex RangeRegex(
|
|
"range[[:blank:]]*([[:alpha:]_]*)?[[:blank:]]*=[[:"
|
|
"blank:]]*(\\+[[:digit:]]+)?[[:blank:]]*(->[[:blank:]"
|
|
"]*[\\+\\:[:digit:]]+)?");
|
|
|
|
std::map<std::string, SmallVector<TestSelectionRange, 8>> GroupedRanges;
|
|
|
|
LangOptions LangOpts;
|
|
LangOpts.CPlusPlus = 1;
|
|
LangOpts.CPlusPlus11 = 1;
|
|
Lexer Lex(SourceLocation::getFromRawEncoding(0), LangOpts, Source.begin(),
|
|
Source.begin(), Source.end());
|
|
Lex.SetCommentRetentionState(true);
|
|
Token Tok;
|
|
for (Lex.LexFromRawLexer(Tok); Tok.isNot(tok::eof);
|
|
Lex.LexFromRawLexer(Tok)) {
|
|
if (Tok.isNot(tok::comment))
|
|
continue;
|
|
StringRef Comment =
|
|
Source.substr(Tok.getLocation().getRawEncoding(), Tok.getLength());
|
|
SmallVector<StringRef, 4> Matches;
|
|
// Try to detect mistyped 'range:' comments to ensure tests don't miss
|
|
// anything.
|
|
auto DetectMistypedCommand = [&]() -> bool {
|
|
if (Comment.contains_lower("range") && Comment.contains("=") &&
|
|
!Comment.contains_lower("run") && !Comment.contains("CHECK")) {
|
|
llvm::errs() << "error: suspicious comment '" << Comment
|
|
<< "' that "
|
|
"resembles the range command found\n";
|
|
llvm::errs() << "note: please reword if this isn't a range command\n";
|
|
}
|
|
return false;
|
|
};
|
|
// Allow CHECK: comments to contain range= commands.
|
|
if (!RangeRegex.match(Comment, &Matches) || Comment.contains("CHECK")) {
|
|
if (DetectMistypedCommand())
|
|
return None;
|
|
continue;
|
|
}
|
|
unsigned Offset = Tok.getEndLoc().getRawEncoding();
|
|
unsigned ColumnOffset = 0;
|
|
if (!Matches[2].empty()) {
|
|
// Don't forget to drop the '+'!
|
|
if (Matches[2].drop_front().getAsInteger(10, ColumnOffset))
|
|
assert(false && "regex should have produced a number");
|
|
}
|
|
Offset = addColumnOffset(Source, Offset, ColumnOffset);
|
|
unsigned EndOffset;
|
|
|
|
if (!Matches[3].empty()) {
|
|
static const Regex EndLocRegex(
|
|
"->[[:blank:]]*(\\+[[:digit:]]+):([[:digit:]]+)");
|
|
SmallVector<StringRef, 4> EndLocMatches;
|
|
if (!EndLocRegex.match(Matches[3], &EndLocMatches)) {
|
|
if (DetectMistypedCommand())
|
|
return None;
|
|
continue;
|
|
}
|
|
unsigned EndLineOffset = 0, EndColumn = 0;
|
|
if (EndLocMatches[1].drop_front().getAsInteger(10, EndLineOffset) ||
|
|
EndLocMatches[2].getAsInteger(10, EndColumn))
|
|
assert(false && "regex should have produced a number");
|
|
EndOffset = addEndLineOffsetAndEndColumn(Source, Offset, EndLineOffset,
|
|
EndColumn);
|
|
} else {
|
|
EndOffset = Offset;
|
|
}
|
|
TestSelectionRange Range = {Offset, EndOffset};
|
|
auto It = GroupedRanges.insert(std::make_pair(
|
|
Matches[1].str(), SmallVector<TestSelectionRange, 8>{Range}));
|
|
if (!It.second)
|
|
It.first->second.push_back(Range);
|
|
}
|
|
if (GroupedRanges.empty()) {
|
|
llvm::errs() << "error: -selection=test:" << Filename
|
|
<< ": no 'range' commands";
|
|
return None;
|
|
}
|
|
|
|
TestSelectionRangesInFile TestRanges = {Filename.str(), {}};
|
|
for (auto &Group : GroupedRanges)
|
|
TestRanges.GroupedRanges.push_back({Group.first, std::move(Group.second)});
|
|
return std::move(TestRanges);
|
|
}
|
|
|
|
} // end namespace refactor
|
|
} // end namespace clang
|