1 //===- ClangDiff.cpp - compare source files by AST nodes ------*- C++ -*- -===//
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
7 //===----------------------------------------------------------------------===//
9 // This file implements a tool for syntax tree based comparison using
12 //===----------------------------------------------------------------------===//
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"
20 using namespace clang
;
21 using namespace clang::tooling
;
23 static cl::OptionCategory
ClangDiffCategory("clang-diff options");
27 cl::desc("Print the internal representation of the AST."),
28 cl::init(false), cl::cat(ClangDiffCategory
));
30 static cl::opt
<bool> ASTDumpJson(
32 cl::desc("Print the internal representation of the AST as JSON."),
33 cl::init(false), cl::cat(ClangDiffCategory
));
35 static cl::opt
<bool> PrintMatches("dump-matches",
36 cl::desc("Print the matched nodes."),
37 cl::init(false), cl::cat(ClangDiffCategory
));
39 static cl::opt
<bool> HtmlDiff("html",
40 cl::desc("Output a side-by-side diff in HTML."),
41 cl::init(false), cl::cat(ClangDiffCategory
));
43 static cl::opt
<std::string
> SourcePath(cl::Positional
, cl::desc("<source>"),
45 cl::cat(ClangDiffCategory
));
47 static cl::opt
<std::string
> DestinationPath(cl::Positional
,
48 cl::desc("<destination>"),
50 cl::cat(ClangDiffCategory
));
52 static cl::opt
<std::string
> StopAfter("stop-diff-after",
53 cl::desc("<topdown|bottomup>"),
54 cl::Optional
, cl::init(""),
55 cl::cat(ClangDiffCategory
));
57 static cl::opt
<int> MaxSize("s", cl::desc("<maxsize>"), cl::Optional
,
58 cl::init(-1), cl::cat(ClangDiffCategory
));
60 static cl::opt
<std::string
> BuildPath("p", cl::desc("Build path"), cl::init(""),
61 cl::Optional
, cl::cat(ClangDiffCategory
));
63 static cl::list
<std::string
> ArgsAfter(
65 cl::desc("Additional argument to append to the compiler command line"),
66 cl::cat(ClangDiffCategory
));
68 static cl::list
<std::string
> ArgsBefore(
70 cl::desc("Additional argument to prepend to the compiler command line"),
71 cl::cat(ClangDiffCategory
));
73 static void addExtraArgs(std::unique_ptr
<CompilationDatabase
> &Compilations
) {
76 auto AdjustingCompilations
=
77 std::make_unique
<ArgumentsAdjustingCompilations
>(
78 std::move(Compilations
));
79 AdjustingCompilations
->appendArgumentsAdjuster(
80 getInsertArgumentAdjuster(ArgsBefore
, ArgumentInsertPosition::BEGIN
));
81 AdjustingCompilations
->appendArgumentsAdjuster(
82 getInsertArgumentAdjuster(ArgsAfter
, ArgumentInsertPosition::END
));
83 Compilations
= std::move(AdjustingCompilations
);
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 BuildPath
.empty() ? Filename
: BuildPath
, ErrorMessage
);
96 << "Error while trying to load a compilation database, running "
100 std::make_unique
<clang::tooling::FixedCompilationDatabase
>(
101 ".", std::vector
<std::string
>());
104 addExtraArgs(Compilations
);
105 std::array
<std::string
, 1> Files
= {{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())
111 return std::move(ASTs
[0]);
114 static char hexdigit(int N
) { return N
&= 0xf, N
+ (N
< 10 ? '0' : 'a' - 10); }
116 static const char HtmlDiffHeader
[] = R
"(
119 <meta charset='utf-8'/>
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; }
131 padding: 0 0 0.5% 0.5%;
132 border: solid 2px LightGrey;
137 <script type='text/javascript'>
139 function clearHighlight() {
140 while (highlightStack.length) {
141 var [l, r] = highlightStack.pop()
142 document.getElementById(l).style.backgroundColor = 'inherit'
144 document.getElementById(r).style.backgroundColor = 'inherit'
147 function highlight(event) {
148 var id = event.target['id']
151 function doHighlight(id) {
153 source = document.getElementById(id)
154 if (!source.attributes['tid'])
157 while (mapped && mapped.parentElement && mapped.attributes['tid'].value.substr(1) === '-1')
158 mapped = mapped.parentElement
159 var tid = null, target = null
161 tid = mapped.attributes['tid'].value
162 target = document.getElementById(tid)
164 if (source.parentElement && source.parentElement.classList.contains('code'))
166 source.style.backgroundColor = 'lightgrey'
167 source.scrollIntoView()
169 if (mapped === source)
170 target.style.backgroundColor = 'lightgrey'
171 target.scrollIntoView()
173 highlightStack.push([id, tid])
174 location.hash = '#' + id
176 function scrollToBoth() {
177 doHighlight(location.hash.substr(1))
179 function changed(elem) {
180 return elem.classList.length == 0
182 function nextChangedNode(prefix, increment, number) {
185 var elem = document.getElementById(prefix + number)
186 } while(elem && !changed(elem))
187 return elem ? number : null
189 function handleKey(e) {
190 var down = e.code === "KeyJ
"
191 var up = e.code === "KeyK
"
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
201 number = nextChangedNode(prefix, increment, number)
202 var elem = document.getElementById(prefix + number)
204 while (elem.parentElement && changed(elem.parentElement))
205 elem = elem.parentElement
206 number = elem.id.substr(1)
208 } while ((down && id !== 'R0' && oldelem.contains(elem)))
211 elem = document.getElementById(prefix + number)
212 doHighlight(prefix + number)
214 window.onload = scrollToBoth
215 window.onkeydown = handleKey
218 <div onclick='highlight(event)'>
221 static void printHtml(raw_ostream
&OS
, char C
) {
243 static void printHtml(raw_ostream
&OS
, const StringRef Str
) {
248 static std::string
getChangeKindAbbr(diff::ChangeKind Kind
) {
260 case diff::UpdateMove
:
263 llvm_unreachable("Invalid enumeration value.");
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(Tree
, Id
);
285 std::tie(Begin
, End
) = Tree
.getSourceRangeOffsets(Node
);
286 const SourceManager
&SrcMgr
= Tree
.getASTContext().getSourceManager();
287 auto Code
= SrcMgr
.getBufferOrFake(SrcMgr
.getMainFileID()).getBuffer();
288 for (; Offset
< Begin
; ++Offset
)
289 printHtml(OS
, Code
[Offset
]);
290 OS
<< "<span id='" << MyTag
<< Id
<< "' "
291 << "tid='" << OtherTag
<< TargetId
<< "' ";
293 printHtml(OS
, Node
.getTypeLabel());
294 OS
<< "\n" << LeftId
<< " -> " << RightId
;
295 std::string Value
= Tree
.getNodeValue(Node
);
296 if (!Value
.empty()) {
298 printHtml(OS
, Value
);
301 if (Node
.Change
!= diff::None
)
302 OS
<< " class='" << getChangeKindAbbr(Node
.Change
) << "'";
305 for (diff::NodeId Child
: Node
.Children
)
306 Offset
= printHtmlForNode(OS
, Diff
, Tree
, IsLeft
, Child
, Offset
);
308 for (; Offset
< End
; ++Offset
)
309 printHtml(OS
, Code
[Offset
]);
310 if (Id
== Tree
.getRootId()) {
312 for (; Offset
< End
; ++Offset
)
313 printHtml(OS
, Code
[Offset
]);
319 static void printJsonString(raw_ostream
&OS
, const StringRef Str
) {
320 for (signed char C
: Str
) {
335 if ('\x00' <= C && C <= '\x1f') {
336 OS << R"(\u00
)" << hexdigit(C >> 4) << hexdigit(C);
344 static void printNodeAttributes(raw_ostream &OS, diff::SyntaxTree &Tree,
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(N
);
353 if (!Value
.empty()) {
354 OS
<< R
"(,"value
":")";
355 printJsonString(OS, Value);
360 static void printNodeAsJson(raw_ostream &OS, diff::SyntaxTree &Tree,
362 const diff::Node &N = Tree.getNode(Id);
364 printNodeAttributes(OS, Tree, Id);
365 auto Identifier = N.getIdentifier();
366 auto QualifiedIdentifier = N.getQualifiedIdentifier();
368 OS << R"(,"identifier":")";
369 printJsonString(OS, *Identifier);
371 if (QualifiedIdentifier && *Identifier != *QualifiedIdentifier) {
372 OS << R"(,"qualified_identifier":")";
373 printJsonString(OS, *QualifiedIdentifier);
377 OS << R"(,"children":[)";
378 if (N.Children.size() > 0) {
379 printNodeAsJson(OS, Tree, N.Children[0]);
380 for (size_t I = 1, E = N.Children.size(); I < E; ++I) {
382 printNodeAsJson(OS, Tree, N.Children[I]);
388 static void printNode(raw_ostream &OS, diff::SyntaxTree &Tree,
390 if (Id.isInvalid()) {
394 OS << Tree.getNode(Id).getTypeLabel();
395 std::string Value = Tree.getNodeValue(Id);
398 OS << "(" << Id << ")";
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)
405 printNode(OS, Tree, Id);
410 static void printDstChange(raw_ostream &OS, diff::ASTDiff &Diff,
411 diff::SyntaxTree &SrcTree, diff::SyntaxTree &DstTree,
413 const diff::Node &DstNode = DstTree.getNode(Dst);
414 diff::NodeId Src = Diff.getMapped(DstTree, Dst);
415 switch (DstNode.Change) {
419 llvm_unreachable("The destination tree can't have deletions
.");
422 printNode(OS, SrcTree, Src);
423 OS << " to
" << DstTree.getNodeValue(Dst) << "\n";
427 case diff::UpdateMove:
428 if (DstNode.Change == diff::Insert)
430 else if (DstNode.Change == diff::Move)
432 else if (DstNode.Change == diff::UpdateMove)
433 OS << "Update
and Move
";
435 printNode(OS, DstTree, Dst);
437 printNode(OS, DstTree, DstNode.Parent);
438 OS << " at
" << DstTree.findPositionInParent(Dst) << "\n";
443 int main(int argc, const char **argv) {
444 std::string ErrorMessage;
445 std::unique_ptr<CompilationDatabase> CommonCompilations =
446 FixedCompilationDatabase::loadFromCommandLine(argc, argv, ErrorMessage);
447 if (!CommonCompilations && !ErrorMessage.empty())
448 llvm::errs() << ErrorMessage;
449 cl::HideUnrelatedOptions(ClangDiffCategory);
450 if (!cl::ParseCommandLineOptions(argc, argv)) {
451 cl::PrintOptionValues();
455 addExtraArgs(CommonCompilations);
457 if (ASTDump || ASTDumpJson) {
458 if (!DestinationPath.empty()) {
459 llvm::errs() << "Error
: Please specify exactly one filename
.\n";
462 std::unique_ptr<ASTUnit> AST = getAST(CommonCompilations, SourcePath);
465 diff::SyntaxTree Tree(AST->getASTContext());
467 printTree(llvm::outs(), Tree);
470 llvm::outs() << R"({"filename":")";
471 printJsonString(llvm::outs(), SourcePath
);
472 llvm::outs() << R
"(","root":)";
473 printNodeAsJson(llvm::outs(), Tree, Tree.getRootId());
474 llvm::outs() << "}\n";
478 if (DestinationPath.empty()) {
479 llvm::errs() << "Error
: Exactly two paths are required
.\n";
483 std::unique_ptr<ASTUnit> Src = getAST(CommonCompilations, SourcePath);
484 std::unique_ptr<ASTUnit> Dst = getAST(CommonCompilations, DestinationPath);
488 diff::ComparisonOptions Options;
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";
499 diff::SyntaxTree SrcTree(Src->getASTContext());
500 diff::SyntaxTree DstTree(Dst->getASTContext());
501 diff::ASTDiff Diff(SrcTree, DstTree, Options);
504 llvm::outs() << HtmlDiffHeader << "<pre
>";
505 llvm::outs() << "<div id
='L' class='code'>";
506 printHtmlForNode(llvm::outs(), Diff, SrcTree, true, SrcTree.getRootId(), 0);
507 llvm::outs() << "</div
>";
508 llvm::outs() << "<div id
='R' class='code'>";
509 printHtmlForNode(llvm::outs(), Diff, DstTree, false, DstTree.getRootId(),
511 llvm::outs() << "</div
>";
512 llvm::outs() << "</pre
></div
></body
></html
>\n";
516 for (diff::NodeId Dst : DstTree) {
517 diff::NodeId Src = Diff.getMapped(DstTree, Dst);
518 if (PrintMatches && Src.isValid()) {
519 llvm::outs() << "Match
";
520 printNode(llvm::outs(), SrcTree, Src);
521 llvm::outs() << " to
";
522 printNode(llvm::outs(), DstTree, Dst);
523 llvm::outs() << "\n";
525 printDstChange(llvm::outs(), Diff, SrcTree, DstTree, Dst);
527 for (diff::NodeId Src : SrcTree) {
528 if (Diff.getMapped(SrcTree, Src).isInvalid()) {
529 llvm::outs() << "Delete
";
530 printNode(llvm::outs(), SrcTree, Src);
531 llvm::outs() << "\n";