diff options
Diffstat (limited to 'tools/clang-refactor/TestSupport.cpp')
-rw-r--r-- | tools/clang-refactor/TestSupport.cpp | 392 |
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 |