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