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