summaryrefslogtreecommitdiff
path: root/tools/clang-refactor/TestSupport.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'tools/clang-refactor/TestSupport.cpp')
-rw-r--r--tools/clang-refactor/TestSupport.cpp392
1 files changed, 392 insertions, 0 deletions
diff --git a/tools/clang-refactor/TestSupport.cpp b/tools/clang-refactor/TestSupport.cpp
new file mode 100644
index 000000000000..9331dfd92eb4
--- /dev/null
+++ b/tools/clang-refactor/TestSupport.cpp
@@ -0,0 +1,392 @@
+//===--- TestSupport.cpp - Clang-based refactoring tool -------------------===//
+//
+// The LLVM Compiler Infrastructure
+//
+// This file is distributed under the University of Illinois Open Source
+// License. See LICENSE.TXT for details.
+//
+//===----------------------------------------------------------------------===//
+///
+/// \file
+/// \brief This file implements routines that provide refactoring testing
+/// utilities.
+///
+//===----------------------------------------------------------------------===//
+
+#include "TestSupport.h"
+#include "clang/Basic/DiagnosticError.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 {
+ const FileEntry *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 (const 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 = DiagText.str().str();
+ });
+ }
+ 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 llvm::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 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 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