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