|  | //===--- 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 |