1 | //===- ClangDiff.cpp - compare source files by AST nodes ------*- C++ -*- -===// |
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 | // This file implements a tool for syntax tree based comparison using |
10 | // Tooling/ASTDiff. |
11 | // |
12 | //===----------------------------------------------------------------------===// |
13 | |
14 | #include "clang/Tooling/ASTDiff/ASTDiff.h" |
15 | #include "clang/Tooling/CommonOptionsParser.h" |
16 | #include "clang/Tooling/Tooling.h" |
17 | #include "llvm/Support/CommandLine.h" |
18 | |
19 | using namespace llvm; |
20 | using namespace clang; |
21 | using namespace clang::tooling; |
22 | |
23 | static cl::OptionCategory ClangDiffCategory("clang-diff options" ); |
24 | |
25 | static cl::opt<bool> |
26 | ASTDump("ast-dump" , |
27 | cl::desc("Print the internal representation of the AST." ), |
28 | cl::init(Val: false), cl::cat(ClangDiffCategory)); |
29 | |
30 | static cl::opt<bool> ASTDumpJson( |
31 | "ast-dump-json" , |
32 | cl::desc("Print the internal representation of the AST as JSON." ), |
33 | cl::init(Val: false), cl::cat(ClangDiffCategory)); |
34 | |
35 | static cl::opt<bool> PrintMatches("dump-matches" , |
36 | cl::desc("Print the matched nodes." ), |
37 | cl::init(Val: false), cl::cat(ClangDiffCategory)); |
38 | |
39 | static cl::opt<bool> HtmlDiff("html" , |
40 | cl::desc("Output a side-by-side diff in HTML." ), |
41 | cl::init(Val: false), cl::cat(ClangDiffCategory)); |
42 | |
43 | static cl::opt<std::string> SourcePath(cl::Positional, cl::desc("<source>" ), |
44 | cl::Required, |
45 | cl::cat(ClangDiffCategory)); |
46 | |
47 | static cl::opt<std::string> DestinationPath(cl::Positional, |
48 | cl::desc("<destination>" ), |
49 | cl::Optional, |
50 | cl::cat(ClangDiffCategory)); |
51 | |
52 | static cl::opt<std::string> StopAfter("stop-diff-after" , |
53 | cl::desc("<topdown|bottomup>" ), |
54 | cl::Optional, cl::init(Val: "" ), |
55 | cl::cat(ClangDiffCategory)); |
56 | |
57 | static cl::opt<int> MaxSize("s" , cl::desc("<maxsize>" ), cl::Optional, |
58 | cl::init(Val: -1), cl::cat(ClangDiffCategory)); |
59 | |
60 | static cl::opt<std::string> BuildPath("p" , cl::desc("Build path" ), cl::init(Val: "" ), |
61 | cl::Optional, cl::cat(ClangDiffCategory)); |
62 | |
63 | static cl::list<std::string> ArgsAfter( |
64 | "extra-arg" , |
65 | cl::desc("Additional argument to append to the compiler command line" ), |
66 | cl::cat(ClangDiffCategory)); |
67 | |
68 | static cl::list<std::string> ArgsBefore( |
69 | "extra-arg-before" , |
70 | cl::desc("Additional argument to prepend to the compiler command line" ), |
71 | cl::cat(ClangDiffCategory)); |
72 | |
73 | static void (std::unique_ptr<CompilationDatabase> &Compilations) { |
74 | if (!Compilations) |
75 | return; |
76 | auto AdjustingCompilations = |
77 | std::make_unique<ArgumentsAdjustingCompilations>( |
78 | args: std::move(Compilations)); |
79 | AdjustingCompilations->appendArgumentsAdjuster( |
80 | Adjuster: getInsertArgumentAdjuster(Extra: ArgsBefore, Pos: ArgumentInsertPosition::BEGIN)); |
81 | AdjustingCompilations->appendArgumentsAdjuster( |
82 | Adjuster: getInsertArgumentAdjuster(Extra: ArgsAfter, Pos: ArgumentInsertPosition::END)); |
83 | Compilations = std::move(AdjustingCompilations); |
84 | } |
85 | |
86 | static std::unique_ptr<ASTUnit> |
87 | getAST(const std::unique_ptr<CompilationDatabase> &CommonCompilations, |
88 | const StringRef Filename) { |
89 | std::string ErrorMessage; |
90 | std::unique_ptr<CompilationDatabase> Compilations; |
91 | if (!CommonCompilations) { |
92 | Compilations = CompilationDatabase::autoDetectFromSource( |
93 | SourceFile: BuildPath.empty() ? Filename : BuildPath, ErrorMessage); |
94 | if (!Compilations) { |
95 | llvm::errs() |
96 | << "Error while trying to load a compilation database, running " |
97 | "without flags.\n" |
98 | << ErrorMessage; |
99 | Compilations = |
100 | std::make_unique<clang::tooling::FixedCompilationDatabase>( |
101 | args: "." , args: std::vector<std::string>()); |
102 | } |
103 | } |
104 | addExtraArgs(Compilations); |
105 | std::array<std::string, 1> Files = {._M_elems: {std::string(Filename)}}; |
106 | ClangTool Tool(Compilations ? *Compilations : *CommonCompilations, Files); |
107 | std::vector<std::unique_ptr<ASTUnit>> ASTs; |
108 | Tool.buildASTs(ASTs); |
109 | if (ASTs.size() != Files.size()) |
110 | return nullptr; |
111 | return std::move(ASTs[0]); |
112 | } |
113 | |
114 | static char hexdigit(int N) { return N &= 0xf, N + (N < 10 ? '0' : 'a' - 10); } |
115 | |
116 | static const char [] = R"( |
117 | <html> |
118 | <head> |
119 | <meta charset='utf-8'/> |
120 | <style> |
121 | span.d { color: red; } |
122 | span.u { color: #cc00cc; } |
123 | span.i { color: green; } |
124 | span.m { font-weight: bold; } |
125 | span { font-weight: normal; color: black; } |
126 | div.code { |
127 | width: 48%; |
128 | height: 98%; |
129 | overflow: scroll; |
130 | float: left; |
131 | padding: 0 0 0.5% 0.5%; |
132 | border: solid 2px LightGrey; |
133 | border-radius: 5px; |
134 | } |
135 | </style> |
136 | </head> |
137 | <script type='text/javascript'> |
138 | highlightStack = [] |
139 | function clearHighlight() { |
140 | while (highlightStack.length) { |
141 | var [l, r] = highlightStack.pop() |
142 | document.getElementById(l).style.backgroundColor = 'inherit' |
143 | if (r[1] != '-') |
144 | document.getElementById(r).style.backgroundColor = 'inherit' |
145 | } |
146 | } |
147 | function highlight(event) { |
148 | var id = event.target['id'] |
149 | doHighlight(id) |
150 | } |
151 | function doHighlight(id) { |
152 | clearHighlight() |
153 | source = document.getElementById(id) |
154 | if (!source.attributes['tid']) |
155 | return |
156 | var mapped = source |
157 | while (mapped && mapped.parentElement && mapped.attributes['tid'].value.substr(1) === '-1') |
158 | mapped = mapped.parentElement |
159 | var tid = null, target = null |
160 | if (mapped) { |
161 | tid = mapped.attributes['tid'].value |
162 | target = document.getElementById(tid) |
163 | } |
164 | if (source.parentElement && source.parentElement.classList.contains('code')) |
165 | return |
166 | source.style.backgroundColor = 'lightgrey' |
167 | source.scrollIntoView() |
168 | if (target) { |
169 | if (mapped === source) |
170 | target.style.backgroundColor = 'lightgrey' |
171 | target.scrollIntoView() |
172 | } |
173 | highlightStack.push([id, tid]) |
174 | location.hash = '#' + id |
175 | } |
176 | function scrollToBoth() { |
177 | doHighlight(location.hash.substr(1)) |
178 | } |
179 | function changed(elem) { |
180 | return elem.classList.length == 0 |
181 | } |
182 | function nextChangedNode(prefix, increment, number) { |
183 | do { |
184 | number += increment |
185 | var elem = document.getElementById(prefix + number) |
186 | } while(elem && !changed(elem)) |
187 | return elem ? number : null |
188 | } |
189 | function handleKey(e) { |
190 | var down = e.code === "KeyJ" |
191 | var up = e.code === "KeyK" |
192 | if (!down && !up) |
193 | return |
194 | var id = highlightStack[0] ? highlightStack[0][0] : 'R0' |
195 | var oldelem = document.getElementById(id) |
196 | var number = parseInt(id.substr(1)) |
197 | var increment = down ? 1 : -1 |
198 | var lastnumber = number |
199 | var prefix = id[0] |
200 | do { |
201 | number = nextChangedNode(prefix, increment, number) |
202 | var elem = document.getElementById(prefix + number) |
203 | if (up && elem) { |
204 | while (elem.parentElement && changed(elem.parentElement)) |
205 | elem = elem.parentElement |
206 | number = elem.id.substr(1) |
207 | } |
208 | } while ((down && id !== 'R0' && oldelem.contains(elem))) |
209 | if (!number) |
210 | number = lastnumber |
211 | elem = document.getElementById(prefix + number) |
212 | doHighlight(prefix + number) |
213 | } |
214 | window.onload = scrollToBoth |
215 | window.onkeydown = handleKey |
216 | </script> |
217 | <body> |
218 | <div onclick='highlight(event)'> |
219 | )" ; |
220 | |
221 | static void printHtml(raw_ostream &OS, char C) { |
222 | switch (C) { |
223 | case '&': |
224 | OS << "&" ; |
225 | break; |
226 | case '<': |
227 | OS << "<" ; |
228 | break; |
229 | case '>': |
230 | OS << ">" ; |
231 | break; |
232 | case '\'': |
233 | OS << "'" ; |
234 | break; |
235 | case '"': |
236 | OS << """ ; |
237 | break; |
238 | default: |
239 | OS << C; |
240 | } |
241 | } |
242 | |
243 | static void printHtml(raw_ostream &OS, const StringRef Str) { |
244 | for (char C : Str) |
245 | printHtml(OS, C); |
246 | } |
247 | |
248 | static std::string getChangeKindAbbr(diff::ChangeKind Kind) { |
249 | switch (Kind) { |
250 | case diff::None: |
251 | return "" ; |
252 | case diff::Delete: |
253 | return "d" ; |
254 | case diff::Update: |
255 | return "u" ; |
256 | case diff::Insert: |
257 | return "i" ; |
258 | case diff::Move: |
259 | return "m" ; |
260 | case diff::UpdateMove: |
261 | return "u m" ; |
262 | } |
263 | llvm_unreachable("Invalid enumeration value." ); |
264 | } |
265 | |
266 | static unsigned printHtmlForNode(raw_ostream &OS, const diff::ASTDiff &Diff, |
267 | diff::SyntaxTree &Tree, bool IsLeft, |
268 | diff::NodeId Id, unsigned Offset) { |
269 | const diff::Node &Node = Tree.getNode(Id); |
270 | char MyTag, OtherTag; |
271 | diff::NodeId LeftId, RightId; |
272 | diff::NodeId TargetId = Diff.getMapped(SourceTree: Tree, Id); |
273 | if (IsLeft) { |
274 | MyTag = 'L'; |
275 | OtherTag = 'R'; |
276 | LeftId = Id; |
277 | RightId = TargetId; |
278 | } else { |
279 | MyTag = 'R'; |
280 | OtherTag = 'L'; |
281 | LeftId = TargetId; |
282 | RightId = Id; |
283 | } |
284 | unsigned Begin, End; |
285 | std::tie(args&: Begin, args&: End) = Tree.getSourceRangeOffsets(N: Node); |
286 | const SourceManager &SrcMgr = Tree.getASTContext().getSourceManager(); |
287 | auto Code = SrcMgr.getBufferOrFake(FID: SrcMgr.getMainFileID()).getBuffer(); |
288 | for (; Offset < Begin; ++Offset) |
289 | printHtml(OS, C: Code[Offset]); |
290 | OS << "<span id='" << MyTag << Id << "' " |
291 | << "tid='" << OtherTag << TargetId << "' " ; |
292 | OS << "title='" ; |
293 | printHtml(OS, Str: Node.getTypeLabel()); |
294 | OS << "\n" << LeftId << " -> " << RightId; |
295 | std::string Value = Tree.getNodeValue(Node); |
296 | if (!Value.empty()) { |
297 | OS << "\n" ; |
298 | printHtml(OS, Str: Value); |
299 | } |
300 | OS << "'" ; |
301 | if (Node.Change != diff::None) |
302 | OS << " class='" << getChangeKindAbbr(Kind: Node.Change) << "'" ; |
303 | OS << ">" ; |
304 | |
305 | for (diff::NodeId Child : Node.Children) |
306 | Offset = printHtmlForNode(OS, Diff, Tree, IsLeft, Id: Child, Offset); |
307 | |
308 | for (; Offset < End; ++Offset) |
309 | printHtml(OS, C: Code[Offset]); |
310 | if (Id == Tree.getRootId()) { |
311 | End = Code.size(); |
312 | for (; Offset < End; ++Offset) |
313 | printHtml(OS, C: Code[Offset]); |
314 | } |
315 | OS << "</span>" ; |
316 | return Offset; |
317 | } |
318 | |
319 | static void printJsonString(raw_ostream &OS, const StringRef Str) { |
320 | for (signed char C : Str) { |
321 | switch (C) { |
322 | case '"': |
323 | OS << R"(\")" ; |
324 | break; |
325 | case '\\': |
326 | OS << R"(\\)" ; |
327 | break; |
328 | case '\n': |
329 | OS << R"(\n)" ; |
330 | break; |
331 | case '\t': |
332 | OS << R"(\t)" ; |
333 | break; |
334 | default: |
335 | if ('\x00' <= C && C <= '\x1f') { |
336 | OS << R"(\u00)" << hexdigit(N: C >> 4) << hexdigit(N: C); |
337 | } else { |
338 | OS << C; |
339 | } |
340 | } |
341 | } |
342 | } |
343 | |
344 | static void printNodeAttributes(raw_ostream &OS, diff::SyntaxTree &Tree, |
345 | diff::NodeId Id) { |
346 | const diff::Node &N = Tree.getNode(Id); |
347 | OS << R"("id":)" << int(Id); |
348 | OS << R"(,"type":")" << N.getTypeLabel() << '"'; |
349 | auto Offsets = Tree.getSourceRangeOffsets(N); |
350 | OS << R"(,"begin":)" << Offsets.first; |
351 | OS << R"(,"end":)" << Offsets.second; |
352 | std::string Value = Tree.getNodeValue(Node: N); |
353 | if (!Value.empty()) { |
354 | OS << R"(,"value":")" ; |
355 | printJsonString(OS, Str: Value); |
356 | OS << '"'; |
357 | } |
358 | } |
359 | |
360 | static void printNodeAsJson(raw_ostream &OS, diff::SyntaxTree &Tree, |
361 | diff::NodeId Id) { |
362 | const diff::Node &N = Tree.getNode(Id); |
363 | OS << "{" ; |
364 | printNodeAttributes(OS, Tree, Id); |
365 | auto Identifier = N.getIdentifier(); |
366 | auto QualifiedIdentifier = N.getQualifiedIdentifier(); |
367 | if (Identifier) { |
368 | OS << R"(,"identifier":")" ; |
369 | printJsonString(OS, Str: *Identifier); |
370 | OS << R"(")" ; |
371 | if (QualifiedIdentifier && *Identifier != *QualifiedIdentifier) { |
372 | OS << R"(,"qualified_identifier":")" ; |
373 | printJsonString(OS, Str: *QualifiedIdentifier); |
374 | OS << R"(")" ; |
375 | } |
376 | } |
377 | OS << R"(,"children":[)" ; |
378 | if (N.Children.size() > 0) { |
379 | printNodeAsJson(OS, Tree, Id: N.Children[0]); |
380 | for (size_t I = 1, E = N.Children.size(); I < E; ++I) { |
381 | OS << "," ; |
382 | printNodeAsJson(OS, Tree, Id: N.Children[I]); |
383 | } |
384 | } |
385 | OS << "]}" ; |
386 | } |
387 | |
388 | static void printNode(raw_ostream &OS, diff::SyntaxTree &Tree, |
389 | diff::NodeId Id) { |
390 | if (Id.isInvalid()) { |
391 | OS << "None" ; |
392 | return; |
393 | } |
394 | OS << Tree.getNode(Id).getTypeLabel(); |
395 | std::string Value = Tree.getNodeValue(Id); |
396 | if (!Value.empty()) |
397 | OS << ": " << Value; |
398 | OS << "(" << Id << ")" ; |
399 | } |
400 | |
401 | static void printTree(raw_ostream &OS, diff::SyntaxTree &Tree) { |
402 | for (diff::NodeId Id : Tree) { |
403 | for (int I = 0; I < Tree.getNode(Id).Depth; ++I) |
404 | OS << " " ; |
405 | printNode(OS, Tree, Id); |
406 | OS << "\n" ; |
407 | } |
408 | } |
409 | |
410 | static void printDstChange(raw_ostream &OS, diff::ASTDiff &Diff, |
411 | diff::SyntaxTree &SrcTree, diff::SyntaxTree &DstTree, |
412 | diff::NodeId Dst) { |
413 | const diff::Node &DstNode = DstTree.getNode(Id: Dst); |
414 | diff::NodeId Src = Diff.getMapped(SourceTree: DstTree, Id: Dst); |
415 | switch (DstNode.Change) { |
416 | case diff::None: |
417 | break; |
418 | case diff::Delete: |
419 | llvm_unreachable("The destination tree can't have deletions." ); |
420 | case diff::Update: |
421 | OS << "Update " ; |
422 | printNode(OS, Tree&: SrcTree, Id: Src); |
423 | OS << " to " << DstTree.getNodeValue(Id: Dst) << "\n" ; |
424 | break; |
425 | case diff::Insert: |
426 | case diff::Move: |
427 | case diff::UpdateMove: |
428 | if (DstNode.Change == diff::Insert) |
429 | OS << "Insert" ; |
430 | else if (DstNode.Change == diff::Move) |
431 | OS << "Move" ; |
432 | else if (DstNode.Change == diff::UpdateMove) |
433 | OS << "Update and Move" ; |
434 | OS << " " ; |
435 | printNode(OS, Tree&: DstTree, Id: Dst); |
436 | OS << " into " ; |
437 | printNode(OS, Tree&: DstTree, Id: DstNode.Parent); |
438 | OS << " at " << DstTree.findPositionInParent(Id: Dst) << "\n" ; |
439 | break; |
440 | } |
441 | } |
442 | |
443 | int main(int argc, const char **argv) { |
444 | std::string ErrorMessage; |
445 | std::unique_ptr<CompilationDatabase> CommonCompilations = |
446 | FixedCompilationDatabase::loadFromCommandLine(Argc&: argc, Argv: argv, ErrorMsg&: ErrorMessage); |
447 | if (!CommonCompilations && !ErrorMessage.empty()) |
448 | llvm::errs() << ErrorMessage; |
449 | cl::HideUnrelatedOptions(Category&: ClangDiffCategory); |
450 | if (!cl::ParseCommandLineOptions(argc, argv)) { |
451 | cl::PrintOptionValues(); |
452 | return 1; |
453 | } |
454 | |
455 | addExtraArgs(Compilations&: CommonCompilations); |
456 | |
457 | if (ASTDump || ASTDumpJson) { |
458 | if (!DestinationPath.empty()) { |
459 | llvm::errs() << "Error: Please specify exactly one filename.\n" ; |
460 | return 1; |
461 | } |
462 | std::unique_ptr<ASTUnit> AST = getAST(CommonCompilations, Filename: SourcePath); |
463 | if (!AST) |
464 | return 1; |
465 | diff::SyntaxTree Tree(AST->getASTContext()); |
466 | if (ASTDump) { |
467 | printTree(OS&: llvm::outs(), Tree); |
468 | return 0; |
469 | } |
470 | llvm::outs() << R"({"filename":")" ; |
471 | printJsonString(OS&: llvm::outs(), Str: SourcePath); |
472 | llvm::outs() << R"(","root":)" ; |
473 | printNodeAsJson(OS&: llvm::outs(), Tree, Id: Tree.getRootId()); |
474 | llvm::outs() << "}\n" ; |
475 | return 0; |
476 | } |
477 | |
478 | if (DestinationPath.empty()) { |
479 | llvm::errs() << "Error: Exactly two paths are required.\n" ; |
480 | return 1; |
481 | } |
482 | |
483 | std::unique_ptr<ASTUnit> Src = getAST(CommonCompilations, Filename: SourcePath); |
484 | std::unique_ptr<ASTUnit> Dst = getAST(CommonCompilations, Filename: DestinationPath); |
485 | if (!Src || !Dst) |
486 | return 1; |
487 | |
488 | diff::ComparisonOptions Options; |
489 | if (MaxSize != -1) |
490 | Options.MaxSize = MaxSize; |
491 | if (!StopAfter.empty()) { |
492 | if (StopAfter == "topdown" ) |
493 | Options.StopAfterTopDown = true; |
494 | else if (StopAfter != "bottomup" ) { |
495 | llvm::errs() << "Error: Invalid argument for -stop-after\n" ; |
496 | return 1; |
497 | } |
498 | } |
499 | diff::SyntaxTree SrcTree(Src->getASTContext()); |
500 | diff::SyntaxTree DstTree(Dst->getASTContext()); |
501 | diff::ASTDiff Diff(SrcTree, DstTree, Options); |
502 | |
503 | if (HtmlDiff) { |
504 | llvm::outs() << HtmlDiffHeader << "<pre>" ; |
505 | llvm::outs() << "<div id='L' class='code'>" ; |
506 | printHtmlForNode(OS&: llvm::outs(), Diff, Tree&: SrcTree, IsLeft: true, Id: SrcTree.getRootId(), Offset: 0); |
507 | llvm::outs() << "</div>" ; |
508 | llvm::outs() << "<div id='R' class='code'>" ; |
509 | printHtmlForNode(OS&: llvm::outs(), Diff, Tree&: DstTree, IsLeft: false, Id: DstTree.getRootId(), |
510 | Offset: 0); |
511 | llvm::outs() << "</div>" ; |
512 | llvm::outs() << "</pre></div></body></html>\n" ; |
513 | return 0; |
514 | } |
515 | |
516 | for (diff::NodeId Dst : DstTree) { |
517 | diff::NodeId Src = Diff.getMapped(SourceTree: DstTree, Id: Dst); |
518 | if (PrintMatches && Src.isValid()) { |
519 | llvm::outs() << "Match " ; |
520 | printNode(OS&: llvm::outs(), Tree&: SrcTree, Id: Src); |
521 | llvm::outs() << " to " ; |
522 | printNode(OS&: llvm::outs(), Tree&: DstTree, Id: Dst); |
523 | llvm::outs() << "\n" ; |
524 | } |
525 | printDstChange(OS&: llvm::outs(), Diff, SrcTree, DstTree, Dst); |
526 | } |
527 | for (diff::NodeId Src : SrcTree) { |
528 | if (Diff.getMapped(SourceTree: SrcTree, Id: Src).isInvalid()) { |
529 | llvm::outs() << "Delete " ; |
530 | printNode(OS&: llvm::outs(), Tree&: SrcTree, Id: Src); |
531 | llvm::outs() << "\n" ; |
532 | } |
533 | } |
534 | |
535 | return 0; |
536 | } |
537 | |