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