| 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 | |