392 lines · cpp
1//===--- TestSupport.cpp - Clang-based refactoring tool -------------------===//2//3// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.4// See https://llvm.org/LICENSE.txt for license information.5// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception6//7//===----------------------------------------------------------------------===//8///9/// \file10/// This file implements routines that provide refactoring testing11/// utilities.12///13//===----------------------------------------------------------------------===//14 15#include "TestSupport.h"16#include "clang/Basic/DiagnosticError.h"17#include "clang/Basic/FileManager.h"18#include "clang/Basic/SourceManager.h"19#include "clang/Lex/Lexer.h"20#include "llvm/ADT/STLExtras.h"21#include "llvm/Support/Error.h"22#include "llvm/Support/ErrorOr.h"23#include "llvm/Support/LineIterator.h"24#include "llvm/Support/MemoryBuffer.h"25#include "llvm/Support/Regex.h"26#include "llvm/Support/raw_ostream.h"27#include <optional>28 29using namespace llvm;30 31namespace clang {32namespace refactor {33 34void TestSelectionRangesInFile::dump(raw_ostream &OS) const {35 for (const auto &Group : GroupedRanges) {36 OS << "Test selection group '" << Group.Name << "':\n";37 for (const auto &Range : Group.Ranges) {38 OS << " " << Range.Begin << "-" << Range.End << "\n";39 }40 }41}42 43bool TestSelectionRangesInFile::foreachRange(44 const SourceManager &SM,45 llvm::function_ref<void(SourceRange)> Callback) const {46 auto FE = SM.getFileManager().getOptionalFileRef(Filename);47 FileID FID = FE ? SM.translateFile(*FE) : FileID();48 if (!FE || FID.isInvalid()) {49 llvm::errs() << "error: -selection=test:" << Filename50 << " : given file is not in the target TU";51 return true;52 }53 SourceLocation FileLoc = SM.getLocForStartOfFile(FID);54 for (const auto &Group : GroupedRanges) {55 for (const TestSelectionRange &Range : Group.Ranges) {56 // Translate the offset pair to a true source range.57 SourceLocation Start =58 SM.getMacroArgExpandedLocation(FileLoc.getLocWithOffset(Range.Begin));59 SourceLocation End =60 SM.getMacroArgExpandedLocation(FileLoc.getLocWithOffset(Range.End));61 assert(Start.isValid() && End.isValid() && "unexpected invalid range");62 Callback(SourceRange(Start, End));63 }64 }65 return false;66}67 68namespace {69 70void dumpChanges(const tooling::AtomicChanges &Changes, raw_ostream &OS) {71 for (const auto &Change : Changes)72 OS << const_cast<tooling::AtomicChange &>(Change).toYAMLString() << "\n";73}74 75bool areChangesSame(const tooling::AtomicChanges &LHS,76 const tooling::AtomicChanges &RHS) {77 if (LHS.size() != RHS.size())78 return false;79 for (auto I : llvm::zip(LHS, RHS)) {80 if (!(std::get<0>(I) == std::get<1>(I)))81 return false;82 }83 return true;84}85 86bool printRewrittenSources(const tooling::AtomicChanges &Changes,87 raw_ostream &OS) {88 std::set<std::string> Files;89 for (const auto &Change : Changes)90 Files.insert(Change.getFilePath());91 tooling::ApplyChangesSpec Spec;92 Spec.Cleanup = false;93 for (const auto &File : Files) {94 llvm::ErrorOr<std::unique_ptr<llvm::MemoryBuffer>> BufferErr =95 llvm::MemoryBuffer::getFile(File);96 if (!BufferErr) {97 llvm::errs() << "failed to open" << File << "\n";98 return true;99 }100 auto Result = tooling::applyAtomicChanges(File, (*BufferErr)->getBuffer(),101 Changes, Spec);102 if (!Result) {103 llvm::errs() << toString(Result.takeError());104 return true;105 }106 OS << *Result;107 }108 return false;109}110 111class TestRefactoringResultConsumer final112 : public ClangRefactorToolConsumerInterface {113public:114 TestRefactoringResultConsumer(const TestSelectionRangesInFile &TestRanges)115 : TestRanges(TestRanges) {116 Results.push_back({});117 }118 119 ~TestRefactoringResultConsumer() {120 // Ensure all results are checked.121 for (auto &Group : Results) {122 for (auto &Result : Group) {123 if (!Result) {124 (void)llvm::toString(Result.takeError());125 }126 }127 }128 }129 130 void handleError(llvm::Error Err) override { handleResult(std::move(Err)); }131 132 void handle(tooling::AtomicChanges Changes) override {133 handleResult(std::move(Changes));134 }135 136 void handle(tooling::SymbolOccurrences Occurrences) override {137 tooling::RefactoringResultConsumer::handle(std::move(Occurrences));138 }139 140private:141 bool handleAllResults();142 143 void handleResult(Expected<tooling::AtomicChanges> Result) {144 Results.back().push_back(std::move(Result));145 size_t GroupIndex = Results.size() - 1;146 if (Results.back().size() >=147 TestRanges.GroupedRanges[GroupIndex].Ranges.size()) {148 ++GroupIndex;149 if (GroupIndex >= TestRanges.GroupedRanges.size()) {150 if (handleAllResults())151 exit(1); // error has occurred.152 return;153 }154 Results.push_back({});155 }156 }157 158 const TestSelectionRangesInFile &TestRanges;159 std::vector<std::vector<Expected<tooling::AtomicChanges>>> Results;160};161 162std::pair<unsigned, unsigned> getLineColumn(StringRef Filename,163 unsigned Offset) {164 ErrorOr<std::unique_ptr<MemoryBuffer>> ErrOrFile =165 MemoryBuffer::getFile(Filename);166 if (!ErrOrFile)167 return {0, 0};168 StringRef Source = ErrOrFile.get()->getBuffer();169 Source = Source.take_front(Offset);170 size_t LastLine = Source.find_last_of("\r\n");171 return {Source.count('\n') + 1,172 (LastLine == StringRef::npos ? Offset : Offset - LastLine) + 1};173}174 175} // end anonymous namespace176 177bool TestRefactoringResultConsumer::handleAllResults() {178 bool Failed = false;179 for (const auto &Group : llvm::enumerate(Results)) {180 // All ranges in the group must produce the same result.181 std::optional<tooling::AtomicChanges> CanonicalResult;182 std::optional<std::string> CanonicalErrorMessage;183 for (const auto &I : llvm::enumerate(Group.value())) {184 Expected<tooling::AtomicChanges> &Result = I.value();185 std::string ErrorMessage;186 bool HasResult = !!Result;187 if (!HasResult) {188 handleAllErrors(189 Result.takeError(),190 [&](StringError &Err) { ErrorMessage = Err.getMessage(); },191 [&](DiagnosticError &Err) {192 const PartialDiagnosticAt &Diag = Err.getDiagnostic();193 llvm::SmallString<100> DiagText;194 Diag.second.EmitToString(getDiags(), DiagText);195 ErrorMessage = std::string(DiagText);196 });197 }198 if (!CanonicalResult && !CanonicalErrorMessage) {199 if (HasResult)200 CanonicalResult = std::move(*Result);201 else202 CanonicalErrorMessage = std::move(ErrorMessage);203 continue;204 }205 206 // Verify that this result corresponds to the canonical result.207 if (CanonicalErrorMessage) {208 // The error messages must match.209 if (!HasResult && ErrorMessage == *CanonicalErrorMessage)210 continue;211 } else {212 assert(CanonicalResult && "missing canonical result");213 // The results must match.214 if (HasResult && areChangesSame(*Result, *CanonicalResult))215 continue;216 }217 Failed = true;218 // Report the mismatch.219 std::pair<unsigned, unsigned> LineColumn = getLineColumn(220 TestRanges.Filename,221 TestRanges.GroupedRanges[Group.index()].Ranges[I.index()].Begin);222 llvm::errs()223 << "error: unexpected refactoring result for range starting at "224 << LineColumn.first << ':' << LineColumn.second << " in group '"225 << TestRanges.GroupedRanges[Group.index()].Name << "':\n ";226 if (HasResult)227 llvm::errs() << "valid result";228 else229 llvm::errs() << "error '" << ErrorMessage << "'";230 llvm::errs() << " does not match initial ";231 if (CanonicalErrorMessage)232 llvm::errs() << "error '" << *CanonicalErrorMessage << "'\n";233 else234 llvm::errs() << "valid result\n";235 if (HasResult && !CanonicalErrorMessage) {236 llvm::errs() << " Expected to Produce:\n";237 dumpChanges(*CanonicalResult, llvm::errs());238 llvm::errs() << " Produced:\n";239 dumpChanges(*Result, llvm::errs());240 }241 }242 243 // Dump the results:244 const auto &TestGroup = TestRanges.GroupedRanges[Group.index()];245 if (!CanonicalResult) {246 llvm::outs() << TestGroup.Ranges.size() << " '" << TestGroup.Name247 << "' results:\n";248 llvm::outs() << *CanonicalErrorMessage << "\n";249 } else {250 llvm::outs() << TestGroup.Ranges.size() << " '" << TestGroup.Name251 << "' results:\n";252 if (printRewrittenSources(*CanonicalResult, llvm::outs()))253 return true;254 }255 }256 return Failed;257}258 259std::unique_ptr<ClangRefactorToolConsumerInterface>260TestSelectionRangesInFile::createConsumer() const {261 return std::make_unique<TestRefactoringResultConsumer>(*this);262}263 264/// Adds the \p ColumnOffset to file offset \p Offset, without going past a265/// newline.266static unsigned addColumnOffset(StringRef Source, unsigned Offset,267 unsigned ColumnOffset) {268 if (!ColumnOffset)269 return Offset;270 StringRef Substr = Source.drop_front(Offset).take_front(ColumnOffset);271 size_t NewlinePos = Substr.find_first_of("\r\n");272 return Offset +273 (NewlinePos == StringRef::npos ? ColumnOffset : (unsigned)NewlinePos);274}275 276static unsigned addEndLineOffsetAndEndColumn(StringRef Source, unsigned Offset,277 unsigned LineNumberOffset,278 unsigned Column) {279 StringRef Line = Source.drop_front(Offset);280 unsigned LineOffset = 0;281 for (; LineNumberOffset != 0; --LineNumberOffset) {282 size_t NewlinePos = Line.find_first_of("\r\n");283 // Line offset goes out of bounds.284 if (NewlinePos == StringRef::npos)285 break;286 LineOffset += NewlinePos + 1;287 Line = Line.drop_front(NewlinePos + 1);288 }289 // Source now points to the line at +lineOffset;290 size_t LineStart = Source.find_last_of("\r\n", /*From=*/Offset + LineOffset);291 return addColumnOffset(292 Source, LineStart == StringRef::npos ? 0 : LineStart + 1, Column - 1);293}294 295std::optional<TestSelectionRangesInFile>296findTestSelectionRanges(StringRef Filename) {297 ErrorOr<std::unique_ptr<MemoryBuffer>> ErrOrFile =298 MemoryBuffer::getFile(Filename);299 if (!ErrOrFile) {300 llvm::errs() << "error: -selection=test:" << Filename301 << " : could not open the given file";302 return std::nullopt;303 }304 StringRef Source = ErrOrFile.get()->getBuffer();305 306 // See the doc comment for this function for the explanation of this307 // syntax.308 static const Regex RangeRegex(309 "range[[:blank:]]*([[:alpha:]_]*)?[[:blank:]]*=[[:"310 "blank:]]*(\\+[[:digit:]]+)?[[:blank:]]*(->[[:blank:]"311 "]*[\\+\\:[:digit:]]+)?");312 313 std::map<std::string, SmallVector<TestSelectionRange, 8>> GroupedRanges;314 315 LangOptions LangOpts;316 LangOpts.CPlusPlus = 1;317 LangOpts.CPlusPlus11 = 1;318 Lexer Lex(SourceLocation::getFromRawEncoding(0), LangOpts, Source.begin(),319 Source.begin(), Source.end());320 Lex.SetCommentRetentionState(true);321 Token Tok;322 for (Lex.LexFromRawLexer(Tok); Tok.isNot(tok::eof);323 Lex.LexFromRawLexer(Tok)) {324 if (Tok.isNot(tok::comment))325 continue;326 StringRef Comment =327 Source.substr(Tok.getLocation().getRawEncoding(), Tok.getLength());328 SmallVector<StringRef, 4> Matches;329 // Try to detect mistyped 'range:' comments to ensure tests don't miss330 // anything.331 auto DetectMistypedCommand = [&]() -> bool {332 if (Comment.contains_insensitive("range") && Comment.contains("=") &&333 !Comment.contains_insensitive("run") && !Comment.contains("CHECK")) {334 llvm::errs() << "error: suspicious comment '" << Comment335 << "' that "336 "resembles the range command found\n";337 llvm::errs() << "note: please reword if this isn't a range command\n";338 }339 return false;340 };341 // Allow CHECK: comments to contain range= commands.342 if (!RangeRegex.match(Comment, &Matches) || Comment.contains("CHECK")) {343 if (DetectMistypedCommand())344 return std::nullopt;345 continue;346 }347 unsigned Offset = Tok.getEndLoc().getRawEncoding();348 unsigned ColumnOffset = 0;349 if (!Matches[2].empty()) {350 // Don't forget to drop the '+'!351 if (Matches[2].drop_front().getAsInteger(10, ColumnOffset))352 assert(false && "regex should have produced a number");353 }354 Offset = addColumnOffset(Source, Offset, ColumnOffset);355 unsigned EndOffset;356 357 if (!Matches[3].empty()) {358 static const Regex EndLocRegex(359 "->[[:blank:]]*(\\+[[:digit:]]+):([[:digit:]]+)");360 SmallVector<StringRef, 4> EndLocMatches;361 if (!EndLocRegex.match(Matches[3], &EndLocMatches)) {362 if (DetectMistypedCommand())363 return std::nullopt;364 continue;365 }366 unsigned EndLineOffset = 0, EndColumn = 0;367 if (EndLocMatches[1].drop_front().getAsInteger(10, EndLineOffset) ||368 EndLocMatches[2].getAsInteger(10, EndColumn))369 assert(false && "regex should have produced a number");370 EndOffset = addEndLineOffsetAndEndColumn(Source, Offset, EndLineOffset,371 EndColumn);372 } else {373 EndOffset = Offset;374 }375 TestSelectionRange Range = {Offset, EndOffset};376 GroupedRanges[Matches[1].str()].push_back(Range);377 }378 if (GroupedRanges.empty()) {379 llvm::errs() << "error: -selection=test:" << Filename380 << ": no 'range' commands";381 return std::nullopt;382 }383 384 TestSelectionRangesInFile TestRanges = {Filename.str(), {}};385 for (auto &Group : GroupedRanges)386 TestRanges.GroupedRanges.push_back({Group.first, std::move(Group.second)});387 return std::move(TestRanges);388}389 390} // end namespace refactor391} // end namespace clang392