| //===--- 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_insensitive("range") && Comment.contains("=") && |
| !Comment.contains_insensitive("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 |