Alex Lorenz | b54ef6a | 2017-09-14 10:06:52 +0000 | [diff] [blame] | 1 | //===--- TestSupport.cpp - Clang-based refactoring tool -------------------===// |
| 2 | // |
| 3 | // The LLVM Compiler Infrastructure |
| 4 | // |
| 5 | // This file is distributed under the University of Illinois Open Source |
| 6 | // License. See LICENSE.TXT for details. |
| 7 | // |
| 8 | //===----------------------------------------------------------------------===// |
| 9 | /// |
| 10 | /// \file |
| 11 | /// \brief This file implements routines that provide refactoring testing |
| 12 | /// utilities. |
| 13 | /// |
| 14 | //===----------------------------------------------------------------------===// |
| 15 | |
| 16 | #include "TestSupport.h" |
Alex Lorenz | f5ca27c | 2017-10-16 18:28:26 +0000 | [diff] [blame] | 17 | #include "clang/Basic/DiagnosticError.h" |
Alex Lorenz | b54ef6a | 2017-09-14 10:06:52 +0000 | [diff] [blame] | 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" |
Alex Lorenz | 7fe441b | 2017-10-24 17:18:45 +0000 | [diff] [blame] | 23 | #include "llvm/Support/LineIterator.h" |
Alex Lorenz | b54ef6a | 2017-09-14 10:06:52 +0000 | [diff] [blame] | 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 | const FileEntry *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 (const 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 |
Alex Lorenz | f5ca27c | 2017-10-16 18:28:26 +0000 | [diff] [blame] | 111 | : public ClangRefactorToolConsumerInterface { |
Alex Lorenz | b54ef6a | 2017-09-14 10:06:52 +0000 | [diff] [blame] | 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) { |
Alex Lorenz | f5ca27c | 2017-10-16 18:28:26 +0000 | [diff] [blame] | 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 = DiagText.str().str(); |
| 195 | }); |
Alex Lorenz | b54ef6a | 2017-09-14 10:06:52 +0000 | [diff] [blame] | 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) { |
Alex Lorenz | 7fe441b | 2017-10-24 17:18:45 +0000 | [diff] [blame] | 245 | llvm::outs() << TestGroup.Ranges.size() << " '" << TestGroup.Name |
Alex Lorenz | b54ef6a | 2017-09-14 10:06:52 +0000 | [diff] [blame] | 246 | << "' results:\n"; |
Alex Lorenz | 7fe441b | 2017-10-24 17:18:45 +0000 | [diff] [blame] | 247 | llvm::outs() << *CanonicalErrorMessage << "\n"; |
Alex Lorenz | b54ef6a | 2017-09-14 10:06:52 +0000 | [diff] [blame] | 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 | |
Alex Lorenz | f5ca27c | 2017-10-16 18:28:26 +0000 | [diff] [blame] | 258 | std::unique_ptr<ClangRefactorToolConsumerInterface> |
Alex Lorenz | b54ef6a | 2017-09-14 10:06:52 +0000 | [diff] [blame] | 259 | TestSelectionRangesInFile::createConsumer() const { |
| 260 | return llvm::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 | |
Alex Lorenz | 7fe441b | 2017-10-24 17:18:45 +0000 | [diff] [blame] | 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 | |
Alex Lorenz | b54ef6a | 2017-09-14 10:06:52 +0000 | [diff] [blame] | 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 | |
Alex Lorenz | b54ef6a | 2017-09-14 10:06:52 +0000 | [diff] [blame] | 305 | // See the doc comment for this function for the explanation of this |
| 306 | // syntax. |
| 307 | static Regex RangeRegex("range[[:blank:]]*([[:alpha:]_]*)?[[:blank:]]*=[[:" |
Alex Lorenz | 7fe441b | 2017-10-24 17:18:45 +0000 | [diff] [blame] | 308 | "blank:]]*(\\+[[:digit:]]+)?[[:blank:]]*(->[[:blank:]" |
| 309 | "]*[\\+\\:[:digit:]]+)?"); |
Alex Lorenz | b54ef6a | 2017-09-14 10:06:52 +0000 | [diff] [blame] | 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; |
Alex Lorenz | 7fe441b | 2017-10-24 17:18:45 +0000 | [diff] [blame] | 327 | // Try to detect mistyped 'range:' comments to ensure tests don't miss |
| 328 | // anything. |
| 329 | auto DetectMistypedCommand = [&]() -> bool { |
Alex Lorenz | b54ef6a | 2017-09-14 10:06:52 +0000 | [diff] [blame] | 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"; |
Alex Lorenz | b54ef6a | 2017-09-14 10:06:52 +0000 | [diff] [blame] | 336 | } |
Alex Lorenz | 7fe441b | 2017-10-24 17:18:45 +0000 | [diff] [blame] | 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; |
Alex Lorenz | b54ef6a | 2017-09-14 10:06:52 +0000 | [diff] [blame] | 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 | } |
Alex Lorenz | b54ef6a | 2017-09-14 10:06:52 +0000 | [diff] [blame] | 352 | Offset = addColumnOffset(Source, Offset, ColumnOffset); |
Alex Lorenz | 7fe441b | 2017-10-24 17:18:45 +0000 | [diff] [blame] | 353 | unsigned EndOffset; |
| 354 | |
| 355 | if (!Matches[3].empty()) { |
| 356 | static 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}; |
Alex Lorenz | b54ef6a | 2017-09-14 10:06:52 +0000 | [diff] [blame] | 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 |