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(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(false), cl::cat(ClangDiffCategory)); 34 35 static cl::opt<bool> PrintMatches("dump-matches", 36 cl::desc("Print the matched nodes."), 37 cl::init(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(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(""), 55 cl::cat(ClangDiffCategory)); 56 57 static cl::opt<int> MaxSize("s", cl::desc("<maxsize>"), cl::Optional, 58 cl::init(-1), cl::cat(ClangDiffCategory)); 59 60 static cl::opt<std::string> BuildPath("p", cl::desc("Build path"), cl::init(""), 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 addExtraArgs(std::unique_ptr<CompilationDatabase> &Compilations) { 74 if (!Compilations) 75 return; 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); 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 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 ".", std::vector<std::string>()); 102 } 103 } 104 addExtraArgs(Compilations); 105 std::array<std::string, 1> Files = {{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 HtmlDiffHeader[] = 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(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(Begin, End) = Tree.getSourceRangeOffsets(Node); 286 const SourceManager &SrcMgr = Tree.getASTContext().getSourceManager(); 287 auto Code = SrcMgr.getBuffer(SrcMgr.getMainFileID())->getBuffer(); 288 for (; Offset < Begin; ++Offset) 289 printHtml(OS, Code[Offset]); 290 OS << "<span id='" << MyTag << Id << "' " 291 << "tid='" << OtherTag << TargetId << "' "; 292 OS << "title='"; 293 printHtml(OS, Node.getTypeLabel()); 294 OS << "\n" << LeftId << " -> " << RightId; 295 std::string Value = Tree.getNodeValue(Node); 296 if (!Value.empty()) { 297 OS << "\n"; 298 printHtml(OS, Value); 299 } 300 OS << "'"; 301 if (Node.Change != diff::None) 302 OS << " class='" << getChangeKindAbbr(Node.Change) << "'"; 303 OS << ">"; 304 305 for (diff::NodeId Child : Node.Children) 306 Offset = printHtmlForNode(OS, Diff, Tree, IsLeft, Child, Offset); 307 308 for (; Offset < End; ++Offset) 309 printHtml(OS, Code[Offset]); 310 if (Id == Tree.getRootId()) { 311 End = Code.size(); 312 for (; Offset < End; ++Offset) 313 printHtml(OS, 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(C >> 4) << hexdigit(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(N); 353 if (!Value.empty()) { 354 OS << R"(,"value":")"; 355 printJsonString(OS, 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, *Identifier); 370 OS << R"(")"; 371 if (QualifiedIdentifier && *Identifier != *QualifiedIdentifier) { 372 OS << R"(,"qualified_identifier":")"; 373 printJsonString(OS, *QualifiedIdentifier); 374 OS << R"(")"; 375 } 376 } 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) { 381 OS << ","; 382 printNodeAsJson(OS, Tree, 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(Dst); 414 diff::NodeId Src = Diff.getMapped(DstTree, 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, SrcTree, Src); 423 OS << " to " << DstTree.getNodeValue(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, DstTree, Dst); 436 OS << " into "; 437 printNode(OS, DstTree, DstNode.Parent); 438 OS << " at " << DstTree.findPositionInParent(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, argv, ErrorMessage); 447 if (!CommonCompilations && !ErrorMessage.empty()) 448 llvm::errs() << ErrorMessage; 449 cl::HideUnrelatedOptions(ClangDiffCategory); 450 if (!cl::ParseCommandLineOptions(argc, argv)) { 451 cl::PrintOptionValues(); 452 return 1; 453 } 454 455 addExtraArgs(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, SourcePath); 463 if (!AST) 464 return 1; 465 diff::SyntaxTree Tree(AST->getASTContext()); 466 if (ASTDump) { 467 printTree(llvm::outs(), Tree); 468 return 0; 469 } 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"; 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, SourcePath); 484 std::unique_ptr<ASTUnit> Dst = getAST(CommonCompilations, 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(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(), 510 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(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"; 524 } 525 printDstChange(llvm::outs(), Diff, SrcTree, DstTree, Dst); 526 } 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"; 532 } 533 } 534 535 return 0; 536 } 537