forked from OSchip/llvm-project
537 lines
16 KiB
C++
537 lines
16 KiB
C++
//===- ClangDiff.cpp - compare source files by AST nodes ------*- C++ -*- -===//
|
|
//
|
|
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
|
|
// See https://llvm.org/LICENSE.txt for license information.
|
|
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
|
|
//
|
|
//===----------------------------------------------------------------------===//
|
|
//
|
|
// This file implements a tool for syntax tree based comparison using
|
|
// Tooling/ASTDiff.
|
|
//
|
|
//===----------------------------------------------------------------------===//
|
|
|
|
#include "clang/Tooling/ASTDiff/ASTDiff.h"
|
|
#include "clang/Tooling/CommonOptionsParser.h"
|
|
#include "clang/Tooling/Tooling.h"
|
|
#include "llvm/Support/CommandLine.h"
|
|
|
|
using namespace llvm;
|
|
using namespace clang;
|
|
using namespace clang::tooling;
|
|
|
|
static cl::OptionCategory ClangDiffCategory("clang-diff options");
|
|
|
|
static cl::opt<bool>
|
|
ASTDump("ast-dump",
|
|
cl::desc("Print the internal representation of the AST."),
|
|
cl::init(false), cl::cat(ClangDiffCategory));
|
|
|
|
static cl::opt<bool> ASTDumpJson(
|
|
"ast-dump-json",
|
|
cl::desc("Print the internal representation of the AST as JSON."),
|
|
cl::init(false), cl::cat(ClangDiffCategory));
|
|
|
|
static cl::opt<bool> PrintMatches("dump-matches",
|
|
cl::desc("Print the matched nodes."),
|
|
cl::init(false), cl::cat(ClangDiffCategory));
|
|
|
|
static cl::opt<bool> HtmlDiff("html",
|
|
cl::desc("Output a side-by-side diff in HTML."),
|
|
cl::init(false), cl::cat(ClangDiffCategory));
|
|
|
|
static cl::opt<std::string> SourcePath(cl::Positional, cl::desc("<source>"),
|
|
cl::Required,
|
|
cl::cat(ClangDiffCategory));
|
|
|
|
static cl::opt<std::string> DestinationPath(cl::Positional,
|
|
cl::desc("<destination>"),
|
|
cl::Optional,
|
|
cl::cat(ClangDiffCategory));
|
|
|
|
static cl::opt<std::string> StopAfter("stop-diff-after",
|
|
cl::desc("<topdown|bottomup>"),
|
|
cl::Optional, cl::init(""),
|
|
cl::cat(ClangDiffCategory));
|
|
|
|
static cl::opt<int> MaxSize("s", cl::desc("<maxsize>"), cl::Optional,
|
|
cl::init(-1), cl::cat(ClangDiffCategory));
|
|
|
|
static cl::opt<std::string> BuildPath("p", cl::desc("Build path"), cl::init(""),
|
|
cl::Optional, cl::cat(ClangDiffCategory));
|
|
|
|
static cl::list<std::string> ArgsAfter(
|
|
"extra-arg",
|
|
cl::desc("Additional argument to append to the compiler command line"),
|
|
cl::cat(ClangDiffCategory));
|
|
|
|
static cl::list<std::string> ArgsBefore(
|
|
"extra-arg-before",
|
|
cl::desc("Additional argument to prepend to the compiler command line"),
|
|
cl::cat(ClangDiffCategory));
|
|
|
|
static void addExtraArgs(std::unique_ptr<CompilationDatabase> &Compilations) {
|
|
if (!Compilations)
|
|
return;
|
|
auto AdjustingCompilations =
|
|
std::make_unique<ArgumentsAdjustingCompilations>(
|
|
std::move(Compilations));
|
|
AdjustingCompilations->appendArgumentsAdjuster(
|
|
getInsertArgumentAdjuster(ArgsBefore, ArgumentInsertPosition::BEGIN));
|
|
AdjustingCompilations->appendArgumentsAdjuster(
|
|
getInsertArgumentAdjuster(ArgsAfter, ArgumentInsertPosition::END));
|
|
Compilations = std::move(AdjustingCompilations);
|
|
}
|
|
|
|
static std::unique_ptr<ASTUnit>
|
|
getAST(const std::unique_ptr<CompilationDatabase> &CommonCompilations,
|
|
const StringRef Filename) {
|
|
std::string ErrorMessage;
|
|
std::unique_ptr<CompilationDatabase> Compilations;
|
|
if (!CommonCompilations) {
|
|
Compilations = CompilationDatabase::autoDetectFromSource(
|
|
BuildPath.empty() ? Filename : BuildPath, ErrorMessage);
|
|
if (!Compilations) {
|
|
llvm::errs()
|
|
<< "Error while trying to load a compilation database, running "
|
|
"without flags.\n"
|
|
<< ErrorMessage;
|
|
Compilations =
|
|
std::make_unique<clang::tooling::FixedCompilationDatabase>(
|
|
".", std::vector<std::string>());
|
|
}
|
|
}
|
|
addExtraArgs(Compilations);
|
|
std::array<std::string, 1> Files = {{std::string(Filename)}};
|
|
ClangTool Tool(Compilations ? *Compilations : *CommonCompilations, Files);
|
|
std::vector<std::unique_ptr<ASTUnit>> ASTs;
|
|
Tool.buildASTs(ASTs);
|
|
if (ASTs.size() != Files.size())
|
|
return nullptr;
|
|
return std::move(ASTs[0]);
|
|
}
|
|
|
|
static char hexdigit(int N) { return N &= 0xf, N + (N < 10 ? '0' : 'a' - 10); }
|
|
|
|
static const char HtmlDiffHeader[] = R"(
|
|
<html>
|
|
<head>
|
|
<meta charset='utf-8'/>
|
|
<style>
|
|
span.d { color: red; }
|
|
span.u { color: #cc00cc; }
|
|
span.i { color: green; }
|
|
span.m { font-weight: bold; }
|
|
span { font-weight: normal; color: black; }
|
|
div.code {
|
|
width: 48%;
|
|
height: 98%;
|
|
overflow: scroll;
|
|
float: left;
|
|
padding: 0 0 0.5% 0.5%;
|
|
border: solid 2px LightGrey;
|
|
border-radius: 5px;
|
|
}
|
|
</style>
|
|
</head>
|
|
<script type='text/javascript'>
|
|
highlightStack = []
|
|
function clearHighlight() {
|
|
while (highlightStack.length) {
|
|
var [l, r] = highlightStack.pop()
|
|
document.getElementById(l).style.backgroundColor = 'inherit'
|
|
if (r[1] != '-')
|
|
document.getElementById(r).style.backgroundColor = 'inherit'
|
|
}
|
|
}
|
|
function highlight(event) {
|
|
var id = event.target['id']
|
|
doHighlight(id)
|
|
}
|
|
function doHighlight(id) {
|
|
clearHighlight()
|
|
source = document.getElementById(id)
|
|
if (!source.attributes['tid'])
|
|
return
|
|
var mapped = source
|
|
while (mapped && mapped.parentElement && mapped.attributes['tid'].value.substr(1) === '-1')
|
|
mapped = mapped.parentElement
|
|
var tid = null, target = null
|
|
if (mapped) {
|
|
tid = mapped.attributes['tid'].value
|
|
target = document.getElementById(tid)
|
|
}
|
|
if (source.parentElement && source.parentElement.classList.contains('code'))
|
|
return
|
|
source.style.backgroundColor = 'lightgrey'
|
|
source.scrollIntoView()
|
|
if (target) {
|
|
if (mapped === source)
|
|
target.style.backgroundColor = 'lightgrey'
|
|
target.scrollIntoView()
|
|
}
|
|
highlightStack.push([id, tid])
|
|
location.hash = '#' + id
|
|
}
|
|
function scrollToBoth() {
|
|
doHighlight(location.hash.substr(1))
|
|
}
|
|
function changed(elem) {
|
|
return elem.classList.length == 0
|
|
}
|
|
function nextChangedNode(prefix, increment, number) {
|
|
do {
|
|
number += increment
|
|
var elem = document.getElementById(prefix + number)
|
|
} while(elem && !changed(elem))
|
|
return elem ? number : null
|
|
}
|
|
function handleKey(e) {
|
|
var down = e.code === "KeyJ"
|
|
var up = e.code === "KeyK"
|
|
if (!down && !up)
|
|
return
|
|
var id = highlightStack[0] ? highlightStack[0][0] : 'R0'
|
|
var oldelem = document.getElementById(id)
|
|
var number = parseInt(id.substr(1))
|
|
var increment = down ? 1 : -1
|
|
var lastnumber = number
|
|
var prefix = id[0]
|
|
do {
|
|
number = nextChangedNode(prefix, increment, number)
|
|
var elem = document.getElementById(prefix + number)
|
|
if (up && elem) {
|
|
while (elem.parentElement && changed(elem.parentElement))
|
|
elem = elem.parentElement
|
|
number = elem.id.substr(1)
|
|
}
|
|
} while ((down && id !== 'R0' && oldelem.contains(elem)))
|
|
if (!number)
|
|
number = lastnumber
|
|
elem = document.getElementById(prefix + number)
|
|
doHighlight(prefix + number)
|
|
}
|
|
window.onload = scrollToBoth
|
|
window.onkeydown = handleKey
|
|
</script>
|
|
<body>
|
|
<div onclick='highlight(event)'>
|
|
)";
|
|
|
|
static void printHtml(raw_ostream &OS, char C) {
|
|
switch (C) {
|
|
case '&':
|
|
OS << "&";
|
|
break;
|
|
case '<':
|
|
OS << "<";
|
|
break;
|
|
case '>':
|
|
OS << ">";
|
|
break;
|
|
case '\'':
|
|
OS << "'";
|
|
break;
|
|
case '"':
|
|
OS << """;
|
|
break;
|
|
default:
|
|
OS << C;
|
|
}
|
|
}
|
|
|
|
static void printHtml(raw_ostream &OS, const StringRef Str) {
|
|
for (char C : Str)
|
|
printHtml(OS, C);
|
|
}
|
|
|
|
static std::string getChangeKindAbbr(diff::ChangeKind Kind) {
|
|
switch (Kind) {
|
|
case diff::None:
|
|
return "";
|
|
case diff::Delete:
|
|
return "d";
|
|
case diff::Update:
|
|
return "u";
|
|
case diff::Insert:
|
|
return "i";
|
|
case diff::Move:
|
|
return "m";
|
|
case diff::UpdateMove:
|
|
return "u m";
|
|
}
|
|
llvm_unreachable("Invalid enumeration value.");
|
|
}
|
|
|
|
static unsigned printHtmlForNode(raw_ostream &OS, const diff::ASTDiff &Diff,
|
|
diff::SyntaxTree &Tree, bool IsLeft,
|
|
diff::NodeId Id, unsigned Offset) {
|
|
const diff::Node &Node = Tree.getNode(Id);
|
|
char MyTag, OtherTag;
|
|
diff::NodeId LeftId, RightId;
|
|
diff::NodeId TargetId = Diff.getMapped(Tree, Id);
|
|
if (IsLeft) {
|
|
MyTag = 'L';
|
|
OtherTag = 'R';
|
|
LeftId = Id;
|
|
RightId = TargetId;
|
|
} else {
|
|
MyTag = 'R';
|
|
OtherTag = 'L';
|
|
LeftId = TargetId;
|
|
RightId = Id;
|
|
}
|
|
unsigned Begin, End;
|
|
std::tie(Begin, End) = Tree.getSourceRangeOffsets(Node);
|
|
const SourceManager &SrcMgr = Tree.getASTContext().getSourceManager();
|
|
auto Code = SrcMgr.getBufferOrFake(SrcMgr.getMainFileID()).getBuffer();
|
|
for (; Offset < Begin; ++Offset)
|
|
printHtml(OS, Code[Offset]);
|
|
OS << "<span id='" << MyTag << Id << "' "
|
|
<< "tid='" << OtherTag << TargetId << "' ";
|
|
OS << "title='";
|
|
printHtml(OS, Node.getTypeLabel());
|
|
OS << "\n" << LeftId << " -> " << RightId;
|
|
std::string Value = Tree.getNodeValue(Node);
|
|
if (!Value.empty()) {
|
|
OS << "\n";
|
|
printHtml(OS, Value);
|
|
}
|
|
OS << "'";
|
|
if (Node.Change != diff::None)
|
|
OS << " class='" << getChangeKindAbbr(Node.Change) << "'";
|
|
OS << ">";
|
|
|
|
for (diff::NodeId Child : Node.Children)
|
|
Offset = printHtmlForNode(OS, Diff, Tree, IsLeft, Child, Offset);
|
|
|
|
for (; Offset < End; ++Offset)
|
|
printHtml(OS, Code[Offset]);
|
|
if (Id == Tree.getRootId()) {
|
|
End = Code.size();
|
|
for (; Offset < End; ++Offset)
|
|
printHtml(OS, Code[Offset]);
|
|
}
|
|
OS << "</span>";
|
|
return Offset;
|
|
}
|
|
|
|
static void printJsonString(raw_ostream &OS, const StringRef Str) {
|
|
for (signed char C : Str) {
|
|
switch (C) {
|
|
case '"':
|
|
OS << R"(\")";
|
|
break;
|
|
case '\\':
|
|
OS << R"(\\)";
|
|
break;
|
|
case '\n':
|
|
OS << R"(\n)";
|
|
break;
|
|
case '\t':
|
|
OS << R"(\t)";
|
|
break;
|
|
default:
|
|
if ('\x00' <= C && C <= '\x1f') {
|
|
OS << R"(\u00)" << hexdigit(C >> 4) << hexdigit(C);
|
|
} else {
|
|
OS << C;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
static void printNodeAttributes(raw_ostream &OS, diff::SyntaxTree &Tree,
|
|
diff::NodeId Id) {
|
|
const diff::Node &N = Tree.getNode(Id);
|
|
OS << R"("id":)" << int(Id);
|
|
OS << R"(,"type":")" << N.getTypeLabel() << '"';
|
|
auto Offsets = Tree.getSourceRangeOffsets(N);
|
|
OS << R"(,"begin":)" << Offsets.first;
|
|
OS << R"(,"end":)" << Offsets.second;
|
|
std::string Value = Tree.getNodeValue(N);
|
|
if (!Value.empty()) {
|
|
OS << R"(,"value":")";
|
|
printJsonString(OS, Value);
|
|
OS << '"';
|
|
}
|
|
}
|
|
|
|
static void printNodeAsJson(raw_ostream &OS, diff::SyntaxTree &Tree,
|
|
diff::NodeId Id) {
|
|
const diff::Node &N = Tree.getNode(Id);
|
|
OS << "{";
|
|
printNodeAttributes(OS, Tree, Id);
|
|
auto Identifier = N.getIdentifier();
|
|
auto QualifiedIdentifier = N.getQualifiedIdentifier();
|
|
if (Identifier) {
|
|
OS << R"(,"identifier":")";
|
|
printJsonString(OS, *Identifier);
|
|
OS << R"(")";
|
|
if (QualifiedIdentifier && *Identifier != *QualifiedIdentifier) {
|
|
OS << R"(,"qualified_identifier":")";
|
|
printJsonString(OS, *QualifiedIdentifier);
|
|
OS << R"(")";
|
|
}
|
|
}
|
|
OS << R"(,"children":[)";
|
|
if (N.Children.size() > 0) {
|
|
printNodeAsJson(OS, Tree, N.Children[0]);
|
|
for (size_t I = 1, E = N.Children.size(); I < E; ++I) {
|
|
OS << ",";
|
|
printNodeAsJson(OS, Tree, N.Children[I]);
|
|
}
|
|
}
|
|
OS << "]}";
|
|
}
|
|
|
|
static void printNode(raw_ostream &OS, diff::SyntaxTree &Tree,
|
|
diff::NodeId Id) {
|
|
if (Id.isInvalid()) {
|
|
OS << "None";
|
|
return;
|
|
}
|
|
OS << Tree.getNode(Id).getTypeLabel();
|
|
std::string Value = Tree.getNodeValue(Id);
|
|
if (!Value.empty())
|
|
OS << ": " << Value;
|
|
OS << "(" << Id << ")";
|
|
}
|
|
|
|
static void printTree(raw_ostream &OS, diff::SyntaxTree &Tree) {
|
|
for (diff::NodeId Id : Tree) {
|
|
for (int I = 0; I < Tree.getNode(Id).Depth; ++I)
|
|
OS << " ";
|
|
printNode(OS, Tree, Id);
|
|
OS << "\n";
|
|
}
|
|
}
|
|
|
|
static void printDstChange(raw_ostream &OS, diff::ASTDiff &Diff,
|
|
diff::SyntaxTree &SrcTree, diff::SyntaxTree &DstTree,
|
|
diff::NodeId Dst) {
|
|
const diff::Node &DstNode = DstTree.getNode(Dst);
|
|
diff::NodeId Src = Diff.getMapped(DstTree, Dst);
|
|
switch (DstNode.Change) {
|
|
case diff::None:
|
|
break;
|
|
case diff::Delete:
|
|
llvm_unreachable("The destination tree can't have deletions.");
|
|
case diff::Update:
|
|
OS << "Update ";
|
|
printNode(OS, SrcTree, Src);
|
|
OS << " to " << DstTree.getNodeValue(Dst) << "\n";
|
|
break;
|
|
case diff::Insert:
|
|
case diff::Move:
|
|
case diff::UpdateMove:
|
|
if (DstNode.Change == diff::Insert)
|
|
OS << "Insert";
|
|
else if (DstNode.Change == diff::Move)
|
|
OS << "Move";
|
|
else if (DstNode.Change == diff::UpdateMove)
|
|
OS << "Update and Move";
|
|
OS << " ";
|
|
printNode(OS, DstTree, Dst);
|
|
OS << " into ";
|
|
printNode(OS, DstTree, DstNode.Parent);
|
|
OS << " at " << DstTree.findPositionInParent(Dst) << "\n";
|
|
break;
|
|
}
|
|
}
|
|
|
|
int main(int argc, const char **argv) {
|
|
std::string ErrorMessage;
|
|
std::unique_ptr<CompilationDatabase> CommonCompilations =
|
|
FixedCompilationDatabase::loadFromCommandLine(argc, argv, ErrorMessage);
|
|
if (!CommonCompilations && !ErrorMessage.empty())
|
|
llvm::errs() << ErrorMessage;
|
|
cl::HideUnrelatedOptions(ClangDiffCategory);
|
|
if (!cl::ParseCommandLineOptions(argc, argv)) {
|
|
cl::PrintOptionValues();
|
|
return 1;
|
|
}
|
|
|
|
addExtraArgs(CommonCompilations);
|
|
|
|
if (ASTDump || ASTDumpJson) {
|
|
if (!DestinationPath.empty()) {
|
|
llvm::errs() << "Error: Please specify exactly one filename.\n";
|
|
return 1;
|
|
}
|
|
std::unique_ptr<ASTUnit> AST = getAST(CommonCompilations, SourcePath);
|
|
if (!AST)
|
|
return 1;
|
|
diff::SyntaxTree Tree(AST->getASTContext());
|
|
if (ASTDump) {
|
|
printTree(llvm::outs(), Tree);
|
|
return 0;
|
|
}
|
|
llvm::outs() << R"({"filename":")";
|
|
printJsonString(llvm::outs(), SourcePath);
|
|
llvm::outs() << R"(","root":)";
|
|
printNodeAsJson(llvm::outs(), Tree, Tree.getRootId());
|
|
llvm::outs() << "}\n";
|
|
return 0;
|
|
}
|
|
|
|
if (DestinationPath.empty()) {
|
|
llvm::errs() << "Error: Exactly two paths are required.\n";
|
|
return 1;
|
|
}
|
|
|
|
std::unique_ptr<ASTUnit> Src = getAST(CommonCompilations, SourcePath);
|
|
std::unique_ptr<ASTUnit> Dst = getAST(CommonCompilations, DestinationPath);
|
|
if (!Src || !Dst)
|
|
return 1;
|
|
|
|
diff::ComparisonOptions Options;
|
|
if (MaxSize != -1)
|
|
Options.MaxSize = MaxSize;
|
|
if (!StopAfter.empty()) {
|
|
if (StopAfter == "topdown")
|
|
Options.StopAfterTopDown = true;
|
|
else if (StopAfter != "bottomup") {
|
|
llvm::errs() << "Error: Invalid argument for -stop-after\n";
|
|
return 1;
|
|
}
|
|
}
|
|
diff::SyntaxTree SrcTree(Src->getASTContext());
|
|
diff::SyntaxTree DstTree(Dst->getASTContext());
|
|
diff::ASTDiff Diff(SrcTree, DstTree, Options);
|
|
|
|
if (HtmlDiff) {
|
|
llvm::outs() << HtmlDiffHeader << "<pre>";
|
|
llvm::outs() << "<div id='L' class='code'>";
|
|
printHtmlForNode(llvm::outs(), Diff, SrcTree, true, SrcTree.getRootId(), 0);
|
|
llvm::outs() << "</div>";
|
|
llvm::outs() << "<div id='R' class='code'>";
|
|
printHtmlForNode(llvm::outs(), Diff, DstTree, false, DstTree.getRootId(),
|
|
0);
|
|
llvm::outs() << "</div>";
|
|
llvm::outs() << "</pre></div></body></html>\n";
|
|
return 0;
|
|
}
|
|
|
|
for (diff::NodeId Dst : DstTree) {
|
|
diff::NodeId Src = Diff.getMapped(DstTree, Dst);
|
|
if (PrintMatches && Src.isValid()) {
|
|
llvm::outs() << "Match ";
|
|
printNode(llvm::outs(), SrcTree, Src);
|
|
llvm::outs() << " to ";
|
|
printNode(llvm::outs(), DstTree, Dst);
|
|
llvm::outs() << "\n";
|
|
}
|
|
printDstChange(llvm::outs(), Diff, SrcTree, DstTree, Dst);
|
|
}
|
|
for (diff::NodeId Src : SrcTree) {
|
|
if (Diff.getMapped(SrcTree, Src).isInvalid()) {
|
|
llvm::outs() << "Delete ";
|
|
printNode(llvm::outs(), SrcTree, Src);
|
|
llvm::outs() << "\n";
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|