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