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