Skip to content

Commit c941d5c

Browse files
ibbempmbittner
authored andcommitted
refactor: Use identity for equals in nodes
Effectively, the semantic of `isSameAs` and `equals` has been exchanged. There is a trade off here: We can't just copy nodes arbitrarily anymore but we gain simpler equality reasoning and some performance. The `FormalDiffGraph` was originally introduced to simplify comparisons but that is now no longer the case. If no future use cases emerge we should consider removing it again.
1 parent c34b567 commit c941d5c

7 files changed

Lines changed: 81 additions & 114 deletions

File tree

src/main/java/org/variantsync/diffdetective/variation/diff/DiffNode.java

Lines changed: 36 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -104,8 +104,7 @@ public String toString() {
104104
* It stores the projection node at each time so that only one instance of {@link Projection}
105105
* per {@link Time} is ever created. This array has to be indexed by {@code Time.ordinal()}
106106
*
107-
* <p>This field is required to allow identity tests of {@link Projection}s with {@code ==} instead
108-
* of {@link Projection#isSameAs}.
107+
* <p>This field is required to allow identity tests of {@link Projection}s with {@code ==}.
109108
*/
110109
private Projection[] projections = new Projection[2];
111110

@@ -824,6 +823,41 @@ public static <T extends VariationNode<T>> DiffNode unchanged(T variationNode) {
824823
return unchanged(DiffNode::unchangedFlat, variationNode);
825824
}
826825

826+
/**
827+
* Returns true if this subtree is exactly equal to {@code other}.
828+
* This check uses equality checks instead of identity.
829+
*/
830+
public boolean isSameAs(DiffNode other) {
831+
return isSameAs(this, other, new HashSet<>());
832+
}
833+
834+
private static boolean isSameAs(DiffNode a, DiffNode b, Set<DiffNode> visited) {
835+
if (!visited.add(a)) {
836+
return true;
837+
}
838+
839+
if (!(
840+
a.getDiffType().equals(b.getDiffType()) &&
841+
a.getNodeType().equals(b.getNodeType()) &&
842+
a.getFromLine().equals(b.getFromLine()) &&
843+
a.getToLine().equals(b.getToLine()) &&
844+
(a.getFormula() == null ? b.getFormula() == null : a.getFormula().equals(b.getFormula())) &&
845+
a.getLabel().equals(b.getLabel())
846+
)) {
847+
return false;
848+
}
849+
850+
Iterator<DiffNode> aIt = a.getAllChildren().iterator();
851+
Iterator<DiffNode> bIt = b.getAllChildren().iterator();
852+
while (aIt.hasNext() && bIt.hasNext()) {
853+
if (!isSameAs(aIt.next(), bIt.next(), visited)) {
854+
return false;
855+
}
856+
}
857+
858+
return aIt.hasNext() == bIt.hasNext();
859+
}
860+
827861
@Override
828862
public String toString() {
829863
String s;
@@ -837,25 +871,4 @@ public String toString() {
837871
}
838872
return s;
839873
}
840-
841-
@Override
842-
public boolean equals(Object o) {
843-
if (this == o) return true;
844-
if (o == null || getClass() != o.getClass()) return false;
845-
DiffNode diffNode = (DiffNode) o;
846-
return diffType == diffNode.diffType && nodeType == diffNode.nodeType && from.equals(diffNode.from) && to.equals(diffNode.to) && Objects.equals(featureMapping, diffNode.featureMapping) && label.equals(diffNode.label);
847-
}
848-
849-
/**
850-
* Compute a hash using all available attributes.
851-
*
852-
* This implementation doesn't strictly adhere to the contract required by {@code Object},
853-
* because some attributes (for example the line numbers) can be changed during the lifetime of
854-
* a {@code DiffNode}. So when using something like a {@code HashSet} the user of {@code
855-
* DiffNode} has to be careful not to change any attributes of a stored {@code DiffNode}.
856-
*/
857-
@Override
858-
public int hashCode() {
859-
return Objects.hash(diffType, nodeType, from, to, featureMapping, label);
860-
}
861874
}

src/main/java/org/variantsync/diffdetective/variation/diff/DiffTree.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -437,6 +437,14 @@ public ConsistencyResult isConsistent() {
437437
return ConsistencyResult.Success();
438438
}
439439

440+
/**
441+
* Returns true if this {@code DiffTree} is exactly equal to {@code other}.
442+
* This check uses equality checks instead of identity.
443+
*/
444+
public boolean isSameAs(DiffTree other) {
445+
return this.getRoot().isSameAs(other.getRoot());
446+
}
447+
440448
@Override
441449
public String toString() {
442450
return "DiffTree of " + source;

src/main/java/org/variantsync/diffdetective/variation/diff/Projection.java

Lines changed: 0 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,6 @@
1818
*
1919
* <p>This class has to be instantiated using {@link DiffNode#projection}.
2020
*
21-
* <p>Implementation note: It's ensured that identity can be checked using {@code ==}. This
22-
* prevents unexpected behaviour if some code uses {@code ==} instead of {@link isSameAs} as
23-
* documented in {@link VariationNode}. Although this is currently guaranteed by all
24-
* implementations of {@link VariationNode} it should still be considered a bug if {@code ==} is
25-
* used to check for identity ({@code null} checks are still allowed).
26-
*
2721
* @see DiffNode#projection
2822
*/
2923
public class Projection extends VariationNode<Projection> {
@@ -148,22 +142,4 @@ public Node getFormula() {
148142
public int getID() {
149143
return getBackingNode().getID();
150144
}
151-
152-
@Override
153-
public boolean isSameAs(Projection other) {
154-
if (other != null && getClass() == other.getClass()) {
155-
Projection otherProjection = (Projection) other;
156-
return time.equals(otherProjection.time) && getBackingNode() == otherProjection.getBackingNode();
157-
} else {
158-
return false;
159-
}
160-
}
161-
162-
@Override
163-
public boolean equals(Object o) {
164-
if (this == o) return true;
165-
if (o == null || getClass() != o.getClass()) return false;
166-
Projection projection = (Projection) o;
167-
return time.equals(projection.time) && getBackingNode().equals(projection.getBackingNode());
168-
}
169145
};

src/main/java/org/variantsync/diffdetective/variation/diff/graph/FormalDiffGraph.java

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -68,20 +68,5 @@ public String toString() {
6868

6969
return b.toString();
7070
}
71-
72-
@Override
73-
public boolean equals(Object o) {
74-
if (this == o) return true;
75-
if (o == null || getClass() != o.getClass()) return false;
76-
final FormalDiffGraph graph = (FormalDiffGraph) o;
77-
78-
return nodes.equals(graph.nodes)
79-
&& edges.equals(graph.edges);
80-
}
81-
82-
@Override
83-
public int hashCode() {
84-
return Objects.hash(nodes, edges);
85-
}
8671
}
8772

src/main/java/org/variantsync/diffdetective/variation/tree/VariationNode.java

Lines changed: 34 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,6 @@
3535
* quite common or because the calling syntax {@code node.algorithm()} makes more sense than the
3636
* alternative {@code Algorithm.run(node)}).
3737
*
38-
* <p>Identity of {@code VariationNode}s shouldn't be tested using {@code ==} because this might be
39-
* a view which is instantiated multiple times for the same node (e.g., it might be that
40-
* {@code getParent() != getParent()}). Instead the method {@link isSameAs} should be used to test
41-
* for identity.
42-
*
4338
* @param <T> the derived type (the type extending this class)
4439
*
4540
* @see assertConsistency
@@ -124,7 +119,7 @@ public List<String> getLabelLines() {
124119
* <p>The following invariant has to hold for all {@code node}s:
125120
* <code>
126121
* for (var child : node.getChildren()) {
127-
* Assert.assertTrue(node.isSameAs(child.getParent(node)))
122+
* Assert.assertTrue(node == child.getParent(node))
128123
* }
129124
* </code>
130125
*
@@ -190,7 +185,7 @@ public T getIfNode() {
190185
* @see getChildren
191186
*/
192187
public boolean isChild(T child) {
193-
return child.getParent().isSameAs(this.upCast());
188+
return child.getParent() == this.upCast();
194189
}
195190

196191
/**
@@ -560,7 +555,7 @@ public void assertConsistency() {
560555
// check consistency of children lists and edges
561556
for (var child : getChildren()) {
562557
Assert.assertTrue(
563-
child.getParent().isSameAs(this.upCast()), () ->
558+
child.getParent() == this.upCast(), () ->
564559
"The parent (" + this + ") of " + child + " is not set correctly");
565560
}
566561
}
@@ -578,19 +573,6 @@ public void assertConsistency() {
578573
*/
579574
public abstract int getID();
580575

581-
/**
582-
* Checks if {@code other} represents the same node as this node.
583-
*
584-
* <p>The difference between this method and {@link equals} is the same as the difference
585-
* between {@code ==} and {@link equals}: {@code isSameAs} respects the identity of the backing
586-
* node and {@link equals} does not.
587-
*
588-
* <p>This method has to be used instead of {@code ==} because multiple instances of this view
589-
* representing the same node might be created (i.e., {@code getParent() == getParent()} might
590-
* not always hold).
591-
*/
592-
public abstract boolean isSameAs(T other);
593-
594576
/**
595577
* Unparses the labels of this subtree into {@code output}.
596578
*
@@ -614,4 +596,35 @@ public void printSourceCode(BufferedWriter output) throws IOException {
614596
output.newLine();
615597
}
616598
}
599+
600+
/**
601+
* Returns true if this subtree is exactly equal to {@code other}.
602+
* This check uses equality checks instead of identity.
603+
*/
604+
public boolean isSameAs(VariationNode<T> other) {
605+
if (!shallowIsSameAs(other)) {
606+
return false;
607+
}
608+
609+
var childIt = getChildren().iterator();
610+
var otherChildIt = other.getChildren().iterator();
611+
while (childIt.hasNext() && otherChildIt.hasNext()) {
612+
if (!childIt.next().isSameAs(otherChildIt.next())) {
613+
return false;
614+
}
615+
}
616+
617+
return childIt.hasNext() == otherChildIt.hasNext();
618+
}
619+
620+
/**
621+
* Returns true if this node is exactly equal to {@code other} without checking any children.
622+
* This check uses equality checks instead of identity.
623+
*/
624+
protected boolean shallowIsSameAs(VariationNode<T> other) {
625+
return
626+
this.getNodeType().equals(other.getNodeType()) &&
627+
this.getLabel().equals(other.getLabel()) &&
628+
this.getLineRange().equals(other.getLineRange());
629+
}
617630
}

src/main/java/org/variantsync/diffdetective/variation/tree/VariationTreeNode.java

Lines changed: 0 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -357,32 +357,6 @@ public VariationTreeNode deepCopy(final Map<VariationTreeNode, VariationTreeNode
357357
return toVariationTree(oldToNew);
358358
}
359359

360-
@Override
361-
public boolean isSameAs(VariationTreeNode other) {
362-
return this == other;
363-
}
364-
365-
@Override
366-
public boolean equals(Object o) {
367-
if (this == o) return true;
368-
if (o == null || getClass() != o.getClass()) return false;
369-
var other = (VariationTreeNode) o;
370-
return nodeType == other.nodeType && lineRange.equals(other.lineRange) && Objects.equals(featureMapping, other.featureMapping) && label.equals(other.label);
371-
}
372-
373-
/**
374-
* Compute a hash using all available attributes.
375-
*
376-
* <p>This implementation doesn't strictly adhere to the contract required by {@code Object},
377-
* because some attributes (for example the line numbers) can be changed during the lifetime of
378-
* a node. So when using something like a {@code HashSet} the user of this class has to be
379-
* careful with any modifications of attributes.
380-
*/
381-
@Override
382-
public int hashCode() {
383-
return Objects.hash(nodeType, lineRange, featureMapping, label);
384-
}
385-
386360
@Override
387361
public String toString() {
388362
String s;

src/test/java/BadVDiffTest.java

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,18 +28,16 @@ public void toGood_after_fromGood_idempotency(String filename) throws IOExceptio
2828
Logger.debug("Testing " + testfile);
2929

3030
final DiffTree initialVDiff = DiffTree.fromFile(testfile, new DiffTreeParseOptions(false, false));
31-
final FormalDiffGraph initialVDiffAsGraph = FormalDiffGraph.fromDiffTree(initialVDiff);
32-
Logger.debug("Initial:" + StringUtils.LINEBREAK + initialVDiffAsGraph);
31+
Logger.debug("Initial:" + StringUtils.LINEBREAK + initialVDiff);
3332
initialVDiff.assertConsistency();
3433

3534
final BadVDiff badDiff = BadVDiff.fromGood(initialVDiff);
3635
Logger.debug("Bad:" + StringUtils.LINEBREAK + badDiff.prettyPrint());
3736

3837
final DiffTree goodDiff = badDiff.toGood();
39-
final FormalDiffGraph goodDiffAsGraph = FormalDiffGraph.fromDiffTree(goodDiff);
40-
Logger.debug("Good:" + StringUtils.LINEBREAK + goodDiffAsGraph);
38+
Logger.debug("Good:" + StringUtils.LINEBREAK + goodDiff);
4139
goodDiff.assertConsistency();
4240

43-
Assertions.assertEquals(initialVDiffAsGraph, goodDiffAsGraph);
41+
Assertions.assertTrue(initialVDiff.isSameAs(goodDiff));
4442
}
4543
}

0 commit comments

Comments
 (0)