blob: 44ea835f93ecfa238e21ab1ea7c043c2f67dec19 [file] [log] [blame]
// Copyright (c) 2018, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'dart:math';
/// [ComparisonNode] defines a simple tree structure that can be used to compare
/// two representations of Dart code.
///
/// Each node contains a textual string and a list of child nodes.
class ComparisonNode {
final String text;
final List<ComparisonNode> children;
ComparisonNode(this.text, [List<ComparisonNode> children])
: children = children ?? <ComparisonNode>[];
factory ComparisonNode.sorted(
String text, Iterable<ComparisonNode> children) =>
ComparisonNode(text, sortList(children));
@override
bool operator ==(Object other) {
if (other is ComparisonNode) {
if (text != other.text) return false;
if (children.length != other.children.length) return false;
for (int i = 0; i < children.length; i++) {
if (children[i] != other.children[i]) return false;
}
return true;
}
return false;
}
String toString({String newline: '\n'}) {
var lines = ['$text'];
var indentedNewline = '$newline ';
for (var child in children) {
lines.add(child.toString(newline: indentedNewline));
}
return lines.join(indentedNewline);
}
static ComparisonNode diff(
ComparisonNode a, ComparisonNode b, String aName, String bName) {
if (a.text == b.text) {
return ComparisonNode(
a.text, diffLists(a.children, b.children, aName, bName));
} else {
return ComparisonNode('Root nodes differ',
[_prefix('In $aName: ', a), _prefix('In $bName: ', b)]);
}
}
static List<ComparisonNode> diffLists(List<ComparisonNode> a,
List<ComparisonNode> b, String aName, String bName) {
// Note: this is an O(n) "poor man's" diff algorithm; it produces optimal
// results if the incoming results are sorted by text or if there is just
// one contiguous hunk of differences. Otherwise it may not find the
// shortest diff. This should be sufficient for our purposes, since we are
// not expecting many diffs.
// We'll exclude common nodes at the beginning of both lists
var shorterLength = min(a.length, b.length);
var commonInitialNodes = 0;
while (commonInitialNodes < shorterLength &&
a[commonInitialNodes] == b[commonInitialNodes]) {
commonInitialNodes++;
}
// Fast exit if a == b
if (commonInitialNodes == a.length && a.length == b.length) {
return [];
}
// We'll exclude common nodes at the end of both lists (note: we don't want
// to overcount by re-testing the common nodes identified above)
var commonFinalNodes = 0;
while (commonInitialNodes + commonFinalNodes < shorterLength &&
a[a.length - commonFinalNodes - 1] ==
b[b.length - commonFinalNodes - 1]) {
commonFinalNodes++;
}
// Walk the remaining nodes starting at the first node that's different,
// matching up nodes by their text.
var aIndex = commonInitialNodes;
var bIndex = commonInitialNodes;
var aEnd = a.length - commonFinalNodes;
var bEnd = b.length - commonFinalNodes;
var result = <ComparisonNode>[];
while (aIndex < aEnd && bIndex < bEnd) {
var comparisonResult = a[aIndex].text.compareTo(b[bIndex].text);
if (comparisonResult < 0) {
// a[aIndex].text sorts before b[bIndex].text. Assume that this means
// a[aIndex] was removed.
result.add(_prefix('Only in $aName: ', a[aIndex++]));
} else if (comparisonResult > 0) {
// b[bIndex].text sorts before a[aIndex].text. Assume that this means
// b[bIndex] was added.
result.add(_prefix('Only in $bName: ', b[bIndex++]));
} else {
// a[aIndex].text matches b[bIndex].text, so diff the nodes if
// necessary.
var aNode = a[aIndex++];
var bNode = b[bIndex++];
if (aNode != bNode) {
result.add(diff(aNode, bNode, aName, bName));
}
}
}
// Deal with any nodes left over.
while (aIndex < aEnd) {
result.add(_prefix('Only in $aName: ', a[aIndex++]));
}
while (bIndex < bEnd) {
result.add(_prefix('Only in $bName: ', b[bIndex++]));
}
// If we get here and we haven't added any nodes, something has gone wrong.
if (result.isEmpty) {
throw StateError('Diff produced empty diff for non-matching lists');
}
return result;
}
static List<ComparisonNode> sortList(Iterable<ComparisonNode> nodes) {
var result = nodes.toList();
result.sort((a, b) => a.text.compareTo(b.text));
return result;
}
static ComparisonNode _prefix(String prefixString, ComparisonNode node) {
return ComparisonNode(prefixString + node.text, node.children);
}
}