Skip to content

Commit 9f567fc

Browse files
committed
Move variation diff construction into a separate file
1 parent c55c99e commit 9f567fc

4 files changed

Lines changed: 307 additions & 307 deletions

File tree

src/main/java/org/variantsync/diffdetective/validation/ConstructionValidation.java

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import org.variantsync.diffdetective.util.Clock;
3434
import org.variantsync.diffdetective.util.Diagnostics;
3535
import org.variantsync.diffdetective.util.IO;
36+
import org.variantsync.diffdetective.variation.diff.Construction;
3637
import org.variantsync.diffdetective.variation.diff.DiffNode;
3738
import org.variantsync.diffdetective.variation.diff.DiffTree;
3839
import org.variantsync.diffdetective.variation.diff.Projection;
@@ -56,8 +57,8 @@
5657
* This experiment computes the variation diff from
5758
* <ol>
5859
* <li>a line matching ({@link DiffTreeParser#createDiffTree Viegener's algorithm}
59-
* <li>a tree matching computed by Gumtree ({@link DiffTree#diffUsingMatching}
60-
* <li>a hybrid matching ({@link DiffTree#improveMatching})
60+
* <li>a tree matching computed by Gumtree ({@link Construction#diffUsingMatching}
61+
* <li>a hybrid matching ({@link Construction#improveMatching})
6162
* </ol>
6263
* compares them using some quality metrics and stores timing statistics.
6364
*
@@ -288,7 +289,7 @@ public boolean analyzeDiffTree(Analysis analysis) throws Exception, DiffParseExc
288289
afterVariationTree.assertConsistency();
289290

290291
clock.start();
291-
final DiffNode newDiffTreeRoot = DiffTree.diffUsingMatching(
292+
final DiffNode newDiffTreeRoot = Construction.diffUsingMatching(
292293
beforeVariationTree.getRoot().projection(BEFORE),
293294
afterVariationTree.getRoot().projection(AFTER),
294295
augmentedMatcher(statistics.diffTree[1])
@@ -301,7 +302,7 @@ public boolean analyzeDiffTree(Analysis analysis) throws Exception, DiffParseExc
301302
final DiffTree improvedDiffTree = analysis.getCurrentDiffTree().deepCopy();
302303
improvedDiffTree.assertConsistency();
303304
clock.start();
304-
improvedDiffTree.improveMatching(augmentedMatcher(statistics.diffTree[2]));
305+
Construction.improveMatching(improvedDiffTree.getRoot(), augmentedMatcher(statistics.diffTree[2]));
305306
statistics.diffTree[2].constructionDuration += clock.getPassedMilliseconds() - statistics.diffTree[2].matchingDuration;
306307
improvedDiffTree.assertConsistency();
307308

Lines changed: 300 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,300 @@
1+
package org.variantsync.diffdetective.variation.diff;
2+
3+
import com.github.gumtreediff.matchers.MappingStore;
4+
import com.github.gumtreediff.matchers.Matcher;
5+
import com.github.gumtreediff.matchers.Matchers;
6+
import com.github.gumtreediff.tree.Tree;
7+
8+
import org.variantsync.diffdetective.diff.text.DiffLineNumber;
9+
import org.variantsync.diffdetective.gumtree.DiffTreeAdapter;
10+
import org.variantsync.diffdetective.gumtree.VariationTreeAdapter;
11+
import org.variantsync.diffdetective.util.Assert;
12+
import org.variantsync.diffdetective.variation.diff.source.VariationTreeDiffSource;
13+
import org.variantsync.diffdetective.variation.diff.traverse.DiffTreeTraversal;
14+
import org.variantsync.diffdetective.variation.tree.VariationNode;
15+
import org.variantsync.diffdetective.variation.tree.VariationTree;
16+
17+
import java.util.HashMap;
18+
import java.util.Map;
19+
20+
import static org.variantsync.diffdetective.variation.diff.DiffType.ADD;
21+
import static org.variantsync.diffdetective.variation.diff.DiffType.NON;
22+
import static org.variantsync.diffdetective.variation.diff.DiffType.REM;
23+
import static org.variantsync.diffdetective.variation.diff.Time.AFTER;
24+
import static org.variantsync.diffdetective.variation.diff.Time.BEFORE;
25+
26+
public class Construction {
27+
/**
28+
* Create a {@link DiffTree} by matching nodes between {@code before} and {@code after} with the
29+
* default GumTree matcher.
30+
*
31+
* @see diffUsingMatching(VariationNode, VariationNode, Matcher)
32+
*/
33+
public static DiffTree diffUsingMatching(VariationTree before, VariationTree after) {
34+
DiffNode root = diffUsingMatching(
35+
before.root(),
36+
after.root(),
37+
Matchers.getInstance().getMatcher()
38+
);
39+
40+
return new DiffTree(root, new VariationTreeDiffSource(before.source(), after.source()));
41+
}
42+
43+
/**
44+
* Create a {@link DiffNode} by matching nodes between {@code before} and {@code after} with
45+
* {@code matcher}. The arguments of this function aren't modified (note the
46+
* {@link diffUsingMatching(DiffNode, VariationNode, Matcher) overload} which modifies
47+
* {@code before} in-place.
48+
*
49+
* @param before the variation tree before an edit
50+
* @param after the variation tree after an edit
51+
* @see diffUsingMatching(DiffNode, VariationNode, Matcher)
52+
*/
53+
public static <A extends VariationNode<A>, B extends VariationNode<B>> DiffNode diffUsingMatching(
54+
VariationNode<A> before,
55+
VariationNode<B> after,
56+
Matcher matcher
57+
) {
58+
return diffUsingMatching(DiffNode.unchanged(before), after, matcher);
59+
}
60+
61+
/**
62+
* Create a {@link DiffNode} by matching nodes between {@code before} and {@code after} with
63+
* {@code matcher}. The result of this function is {@code before} which is modified in-place. In
64+
* contrast, {@code after} is kept in tact.
65+
*
66+
* Warning: Modifications to {@code before} shouldn't concurrently modify {@code after}.
67+
*
68+
* Note: There are currently no guarantees about the line numbers. But it is guaranteed that
69+
* {@link DiffNode#getID} is unique.
70+
*
71+
* @param before the variation tree before an edit
72+
* @param after the variation tree after an edit
73+
* @see "Constructing Variation Diffs Using Tree Diffing Algorithms"
74+
*/
75+
public static <B extends VariationNode<B>> DiffNode diffUsingMatching(
76+
DiffNode before,
77+
VariationNode<B> after,
78+
Matcher matcher
79+
) {
80+
var src = new DiffTreeAdapter(before, BEFORE);
81+
var dst = new VariationTreeAdapter(after);
82+
83+
MappingStore matching = matcher.match(src, dst);
84+
Assert.assertTrue(matching.has(src, dst));
85+
86+
removeUnmapped(matching, src);
87+
for (var child : dst.getChildren()) {
88+
addUnmapped(matching, src.getDiffNode(), (VariationTreeAdapter)child);
89+
}
90+
91+
int[] currentID = new int[1];
92+
DiffTreeTraversal.forAll((node) -> {
93+
node.setFromLine(node.getFromLine().withLineNumberInDiff(currentID[0]));
94+
node.setToLine(node.getToLine().withLineNumberInDiff(currentID[0]));
95+
++currentID[0];
96+
}).visit(before);
97+
98+
return before;
99+
}
100+
101+
/**
102+
* Remove all nodes from the {@code BEFORE} projection which aren't part of a mapping.
103+
*
104+
* @param mappings the matching between the {@code BEFORE} projection of {@code root} some
105+
* variation tree
106+
* @param root the variation diff whose before projection is modified
107+
*/
108+
private static void removeUnmapped(MappingStore mappings, DiffTreeAdapter root) {
109+
for (var node : root.preOrder()) {
110+
Tree dst = mappings.getDstForSrc(node);
111+
if (dst == null || !dst.getLabel().equals(node.getLabel())) {
112+
var diffNode = ((DiffTreeAdapter)node).getDiffNode();
113+
diffNode.diffType = REM;
114+
diffNode.drop(AFTER);
115+
}
116+
}
117+
}
118+
119+
/**
120+
* Recursively adds {@code afterNode} to {@code parent} reusing matched nodes.
121+
*
122+
* The variation diff {@code parent} is modified in-place such that its {@code AFTER}
123+
* projection contains a child equivalent to {@code afterNode} which shares matched nodes with
124+
* the {@code BEFORE} projection of {@code parent}.
125+
*
126+
* @param mappings the matching between the {@code BEFORE} projection of {@code root} and a
127+
* variation tree containing {@code afterNode}
128+
* @param parent the variation diff whose {@code AFTER} projection is modified
129+
* @param afterNode a desired child of {@code parent}'s {@code AFTER} projection
130+
*/
131+
private static void addUnmapped(MappingStore mappings, DiffNode parent, VariationTreeAdapter afterNode) {
132+
VariationNode<?> variationNode = afterNode.getVariationNode();
133+
DiffNode diffNode;
134+
135+
Tree src = mappings.getSrcForDst(afterNode);
136+
if (src == null || !src.getLabel().equals(afterNode.getLabel())) {
137+
int from = variationNode.getLineRange().fromInclusive();
138+
int to = variationNode.getLineRange().toExclusive();
139+
140+
diffNode = new DiffNode(
141+
ADD,
142+
variationNode.getNodeType(),
143+
new DiffLineNumber(DiffLineNumber.InvalidLineNumber, from, from),
144+
new DiffLineNumber(DiffLineNumber.InvalidLineNumber, to, to),
145+
variationNode.getFormula(),
146+
variationNode.getLabelLines()
147+
);
148+
} else {
149+
diffNode = ((DiffTreeAdapter)src).getDiffNode();
150+
if (diffNode.getParent(AFTER) != null) {
151+
// Always drop and reinsert it because it could have moved.
152+
diffNode.drop(AFTER);
153+
}
154+
}
155+
parent.addChild(diffNode, AFTER);
156+
157+
diffNode.removeChildren(AFTER);
158+
for (var child : afterNode.getChildren()) {
159+
addUnmapped(mappings, diffNode, (VariationTreeAdapter)child);
160+
}
161+
}
162+
163+
/**
164+
* Run {@code matcher} on the matching extracted from {@code tree} and modify {@code tree}
165+
* in-place to reflect the new matching.
166+
*
167+
* This is equivalent to {@code diffUsingMatching} except that the existing implicit matching
168+
* is {@link extractMatching extracted} and used as basis for the new matching. Hence, this
169+
* method is mostly an optimisation to avoid a copy of the {@code AFTER} projection of {@code
170+
* tree}.
171+
*
172+
* @see "Constructing Variation Diffs Using Tree Diffing Algorithms"
173+
*/
174+
public static DiffNode improveMatching(DiffNode tree, Matcher matcher) {
175+
var src = new DiffTreeAdapter(tree, BEFORE);
176+
var dst = new DiffTreeAdapter(tree, AFTER);
177+
178+
MappingStore matching = new MappingStore(src, dst);
179+
extractMatching(src, dst, matching);
180+
matcher.match(src, dst, matching);
181+
Assert.assertTrue(matching.has(src, dst));
182+
183+
for (var srcNode : src.preOrder()) {
184+
var dstNode = matching.getDstForSrc(srcNode);
185+
var beforeNode = ((DiffTreeAdapter)srcNode).getDiffNode();
186+
if (dstNode == null || !srcNode.getLabel().equals(dstNode.getLabel())) {
187+
if (beforeNode.isNon()) {
188+
splitNode(beforeNode);
189+
}
190+
191+
Assert.assertTrue(beforeNode.isRem());
192+
} else {
193+
var afterNode = ((DiffTreeAdapter)dstNode).getDiffNode();
194+
195+
if (beforeNode != afterNode) {
196+
if (beforeNode.isNon()) {
197+
splitNode(beforeNode);
198+
}
199+
if (afterNode.isNon()) {
200+
afterNode = splitNode(afterNode);
201+
}
202+
203+
joinNode(beforeNode, afterNode);
204+
}
205+
206+
Assert.assertTrue(beforeNode.isNon());
207+
}
208+
beforeNode.assertConsistency();
209+
}
210+
211+
return tree;
212+
}
213+
214+
/**
215+
* Removes the implicit matching between the {@code BEFORE} and {@code AFTER} projection of
216+
* {@code beforeNode}. This is achieved by copying {@code beforeNode} and reconnecting all
217+
* necessary edges such that the new node exists only after and {@code beforeNode} only exists
218+
* before the edit.
219+
*
220+
* This method doesn't change the {@code BEFORE} and {@code AFTER} projection of {@code
221+
* beforeNode}.
222+
*
223+
* @param beforeNode the node to be split
224+
* @return a copy of {@code beforeNode} existing only after the edit.
225+
*/
226+
private static DiffNode splitNode(DiffNode beforeNode) {
227+
Assert.assertTrue(beforeNode.isNon());
228+
229+
DiffNode afterNode = beforeNode.shallowCopy();
230+
231+
afterNode.diffType = ADD;
232+
beforeNode.diffType = REM;
233+
234+
afterNode.addChildren(beforeNode.removeChildren(AFTER), AFTER);
235+
var afterParent = beforeNode.getParent(AFTER);
236+
afterParent.insertChild(afterNode, afterParent.indexOfChild(beforeNode, AFTER), AFTER);
237+
beforeNode.drop(AFTER);
238+
239+
beforeNode.assertConsistency();
240+
afterNode.assertConsistency();
241+
242+
return afterNode;
243+
}
244+
245+
/**
246+
* Merges {@code afterNode} into {@code beforeNode} such that {@code beforeNode.isNon() ==
247+
* true}. Essentially, an implicit matching is inserted between {@code beforeNode} and {@code
248+
* afterNode}.
249+
*
250+
* This method doesn't change the {@code BEFORE} and {@code AFTER} projection of {@code
251+
* beforeNode}.
252+
*
253+
* @param beforeNode the node which is will exist {@code BEFORE} and {@code AFTER} the edit
254+
* @param afterNode the node which is discarded
255+
*/
256+
private static void joinNode(DiffNode beforeNode, DiffNode afterNode) {
257+
Assert.assertTrue(beforeNode.isRem());
258+
Assert.assertTrue(afterNode.isAdd());
259+
260+
beforeNode.diffType = NON;
261+
262+
beforeNode.addChildren(afterNode.removeChildren(AFTER), AFTER);
263+
264+
var afterParent = afterNode.getParent(AFTER);
265+
afterParent.insertChild(beforeNode, afterParent.indexOfChild(afterNode, AFTER), AFTER);
266+
afterNode.drop(AFTER);
267+
}
268+
269+
/**
270+
* Makes the implicit matching of a {@code DiffTree} explicit.
271+
*
272+
* @param src the source nodes of the matching, must be of the same {@link DiffTree} as {@code
273+
* dst.
274+
* @param dst the destination nodes of the matching, must be of the same {@link DiffTree} as
275+
* {@code src}
276+
* @param result the destination where the matching between {@code src} and {@code dst} is added
277+
*/
278+
private static void extractMatching(
279+
DiffTreeAdapter src,
280+
DiffTreeAdapter dst,
281+
MappingStore result
282+
) {
283+
Map<DiffNode, Tree> matching = new HashMap<>();
284+
285+
for (var srcNode : src.preOrder()) {
286+
DiffNode diffNode = ((DiffTreeAdapter)srcNode).getDiffNode();
287+
if (diffNode.isNon()) {
288+
matching.put(diffNode, srcNode);
289+
}
290+
}
291+
292+
for (var dstNode : dst.preOrder()) {
293+
DiffNode diffNode = ((DiffTreeAdapter)dstNode).getDiffNode();
294+
if (diffNode.isNon()) {
295+
Assert.assertTrue(matching.get(diffNode) != null);
296+
result.addMapping(matching.get(diffNode), dstNode);
297+
}
298+
}
299+
}
300+
}

0 commit comments

Comments
 (0)