diff --git a/src/main/java/org/cqfn/astranaut/core/DifferenceNode.java b/src/main/java/org/cqfn/astranaut/core/DifferenceNode.java index 5cd78f0..b0ceb50 100644 --- a/src/main/java/org/cqfn/astranaut/core/DifferenceNode.java +++ b/src/main/java/org/cqfn/astranaut/core/DifferenceNode.java @@ -117,6 +117,46 @@ public Node getAfter() { return this.getBranch(DifferenceTreeItem::getAfter); } + /** + * Adds an action that replaces the node. + * The position of the node is specified by the index. + * @param index Node index + * @param replacement Child node to be replaced by + * @return Result of operation, @return {@code true} if action was added + */ + public boolean replaceNode(final int index, final Node replacement) { + boolean result = false; + if (index >= 0 && index < this.children.size()) { + final DifferenceTreeItem child = this.children.get(index); + if (child instanceof DifferenceNode) { + this.children.set( + index, + new Replace( + ((DifferenceNode) child).getPrototype(), + replacement + ) + ); + result = true; + } + } + return result; + } + + /** + * Adds an action that replaces the node. + * @param node A node + * @param replacement Child node to be replaced by + * @return Result of operation, @return {@code true} if action was added + */ + public boolean replaceNode(final Node node, final Node replacement) { + boolean result = false; + final int index = this.findChildIndex(node); + if (index >= 0) { + result = this.replaceNode(index, replacement); + } + return result; + } + /** * Adds an action that removes a node by index. * @param index Node index diff --git a/src/main/java/org/cqfn/astranaut/core/Replace.java b/src/main/java/org/cqfn/astranaut/core/Replace.java index 454bf0a..c8e929c 100644 --- a/src/main/java/org/cqfn/astranaut/core/Replace.java +++ b/src/main/java/org/cqfn/astranaut/core/Replace.java @@ -54,8 +54,8 @@ public final class Replace implements Action { /** * Constructor. - * @param before Child element that will be replaced. - * @param after Child element to be replaced by. + * @param before Child element that will be replaced + * @param after Child element to be replaced by */ public Replace(final Node before, final Node after) { this.before = before; diff --git a/src/main/java/org/cqfn/astranaut/core/algorithms/DifferenceTreeBuilder.java b/src/main/java/org/cqfn/astranaut/core/algorithms/DifferenceTreeBuilder.java index 3091bda..c7fcb20 100644 --- a/src/main/java/org/cqfn/astranaut/core/algorithms/DifferenceTreeBuilder.java +++ b/src/main/java/org/cqfn/astranaut/core/algorithms/DifferenceTreeBuilder.java @@ -66,6 +66,9 @@ public DifferenceTreeBuilder(final Node before) { public boolean build(final Node after, final Mapper mapper) { final Mapping mapping = mapper.map(this.root.getPrototype(), after); boolean result = true; + for (final Map.Entry replaced : mapping.getReplaced().entrySet()) { + result = result & this.replaceNode(replaced.getKey(), replaced.getValue()); + } for (final Node deleted : mapping.getDeleted()) { result = result & this.deleteNode(deleted); } @@ -80,6 +83,21 @@ public DifferenceNode getRoot() { return this.root; } + /** + * Adds an action to the difference tree that replaces a node. + * @param node Child element that will be replaced + * @param replacement Child element to be replaced by + * @return Result of operation, {@code true} if action was added + */ + public boolean replaceNode(final Node node, final Node replacement) { + boolean result = false; + final DifferenceNode parent = this.parents.get(node); + if (parent != null) { + result = parent.replaceNode(node, replacement); + } + return result; + } + /** * Adds an action to the difference tree that removes a node. * @param node The node to be removed diff --git a/src/main/java/org/cqfn/astranaut/core/algorithms/mapping/BottomUpMappingAlgorithm.java b/src/main/java/org/cqfn/astranaut/core/algorithms/mapping/BottomUpMappingAlgorithm.java index 3e70db3..24ab76c 100644 --- a/src/main/java/org/cqfn/astranaut/core/algorithms/mapping/BottomUpMappingAlgorithm.java +++ b/src/main/java/org/cqfn/astranaut/core/algorithms/mapping/BottomUpMappingAlgorithm.java @@ -80,6 +80,11 @@ class BottomUpMappingAlgorithm { */ private final Map rtl; + /** + * Map containing replaces nodes. + */ + private final Map replaced; + /** * Set of deleted nodes. */ @@ -98,6 +103,7 @@ class BottomUpMappingAlgorithm { this.right = this.createNodeSet(right); this.ltr = new HashMap<>(); this.rtl = new HashMap<>(); + this.replaced = new HashMap<>(); this.deleted = new HashSet<>(); } @@ -132,6 +138,11 @@ public Node getLeft(final Node node) { return BottomUpMappingAlgorithm.this.rtl.get(node); } + @Override + public Map getReplaced() { + return Collections.unmodifiableMap(BottomUpMappingAlgorithm.this.replaced); + } + @Override public Set getDeleted() { return Collections.unmodifiableSet(BottomUpMappingAlgorithm.this.deleted); @@ -292,16 +303,62 @@ private Node mapPartiallyMappedLeftNode(final Node node) { this.right.remove(related); this.ltr.put(node, related); this.rtl.put(related, node); - final int count = node.getChildCount(); - for (int index = 0; index < count; index = index + 1) { - final Node child = node.getChild(index); - if (!this.ltr.containsKey(child)) { - this.deleted.add(child); - } - } + this.mapChildren(node, related); next = this.parents.get(node); } while (false); this.left.remove(node); return next; } + + /** + * Maps the child nodes of partially mapped nodes. + * @param before Node before changes + * @param after Node after changes + */ + private void mapChildren(final Node before, final Node after) { + final int sign = Integer.compare(before.getChildCount(), after.getChildCount()); + if (sign > 0) { + this.mapChildrenIfDeleted(before); + } else if (sign == 0) { + this.mapChildrenIfReplaced(before, after); + } + } + + /** + * Maps the child nodes of partially mapped nodes if the node before changes + * has the same number of child nodes as the node after changes, i.e., when it is obvious + * that some nodes have been replaced. + * @param before Node before changes + * @param after Node after changes + */ + private void mapChildrenIfReplaced(final Node before, final Node after) { + final int count = before.getChildCount(); + assert count == after.getChildCount(); + for (int index = 0; index < count; index = index + 1) { + final Node first = before.getChild(index); + if (!this.ltr.containsKey(first)) { + final Node second = after.getChild(index); + this.replaced.put(first, second); + this.left.remove(first); + this.right.remove(second); + } + } + } + + /** + * Maps the child nodes of partially mapped nodes if the node before changes + * has more child nodes than the node after changes, i.e., when it is obvious + * that some nodes have been deleted. + * @param before Node before changes + */ + private void mapChildrenIfDeleted(final Node before) { + final int count = before.getChildCount(); + for (int index = 0; index < count; index = index + 1) { + final Node child = before.getChild(index); + if (!this.ltr.containsKey(child)) { + this.deleted.add(child); + this.left.remove(child); + } + } + } } diff --git a/src/main/java/org/cqfn/astranaut/core/algorithms/mapping/Mapping.java b/src/main/java/org/cqfn/astranaut/core/algorithms/mapping/Mapping.java index 2b3fc70..da1eb2e 100644 --- a/src/main/java/org/cqfn/astranaut/core/algorithms/mapping/Mapping.java +++ b/src/main/java/org/cqfn/astranaut/core/algorithms/mapping/Mapping.java @@ -23,6 +23,7 @@ */ package org.cqfn.astranaut.core.algorithms.mapping; +import java.util.Map; import java.util.Set; import org.cqfn.astranaut.core.Node; @@ -49,6 +50,14 @@ public interface Mapping { */ Node getLeft(Node right); + /** + * Returns relationship between the nodes of the 'left' tree that have been replaced + * by nodes of the 'right' tree. + * @return Mapping, where keys are the nodes of the 'left' tree + * and values are the corresponding nodes of the 'right' tree + */ + Map getReplaced(); + /** * Returns the set of nodes of the 'left' tree that need to be removed * to get the 'right' tree. diff --git a/src/test/java/org/cqfn/astranaut/core/algorithms/DifferenceTreeBuilderTest.java b/src/test/java/org/cqfn/astranaut/core/algorithms/DifferenceTreeBuilderTest.java index 06f0c88..993e0f2 100644 --- a/src/test/java/org/cqfn/astranaut/core/algorithms/DifferenceTreeBuilderTest.java +++ b/src/test/java/org/cqfn/astranaut/core/algorithms/DifferenceTreeBuilderTest.java @@ -25,6 +25,8 @@ import org.cqfn.astranaut.core.DifferenceNode; import org.cqfn.astranaut.core.Node; +import org.cqfn.astranaut.core.algorithms.hash.AbsoluteHash; +import org.cqfn.astranaut.core.algorithms.hash.Hash; import org.cqfn.astranaut.core.algorithms.mapping.BottomUpMapper; import org.cqfn.astranaut.core.example.LittleTrees; import org.junit.jupiter.api.Assertions; @@ -36,12 +38,39 @@ * @since 1.1.0 */ class DifferenceTreeBuilderTest { + /** + * Testing the construction of a difference tree with a replaced node. + */ + @Test + void testTreeWithReplacedNode() { + final Node before = LittleTrees.createStatementListWithThreeChildren( + LittleTrees.createIntegerLiteral(2) + ); + final Node after = LittleTrees.createStatementListWithThreeChildren( + LittleTrees.createVariable("x") + ); + final DifferenceTreeBuilder builder = new DifferenceTreeBuilder(before); + final boolean result = builder.build(after, new BottomUpMapper()); + Assertions.assertTrue(result); + final DifferenceNode diff = builder.getRoot(); + final Node expected = LittleTrees.createTreeWithReplaceAction(); + final Hash hash = new AbsoluteHash(); + final int diffhash = hash.calculate(diff); + final int expectedhash = hash.calculate(expected); + Assertions.assertEquals(expectedhash, diffhash); + Assertions.assertTrue(expected.deepCompare(diff)); + Assertions.assertTrue(before.deepCompare(diff.getBefore())); + Assertions.assertTrue(after.deepCompare(diff.getAfter())); + } + /** * Testing the construction of a difference tree with a deleted node. */ @Test void testTreeWithDeletedNode() { - final Node before = LittleTrees.createStatementListWithThreeChildren(); + final Node before = LittleTrees.createStatementListWithThreeChildren( + LittleTrees.createIntegerLiteral(2) + ); final Node after = LittleTrees.createStatementListWithTwoChildren(); final DifferenceTreeBuilder builder = new DifferenceTreeBuilder(before); final boolean result = builder.build(after, new BottomUpMapper()); @@ -61,7 +90,9 @@ void testTreeWithDeletedNode() { @Test void testTreeWithDeletedNodeInDepth() { final Node before = LittleTrees.createStatementBlock( - LittleTrees.createStatementListWithThreeChildren() + LittleTrees.createStatementListWithThreeChildren( + LittleTrees.createIntegerLiteral(2) + ) ); final Node after = LittleTrees.createStatementBlock( LittleTrees.createStatementListWithTwoChildren() diff --git a/src/test/java/org/cqfn/astranaut/core/algorithms/mapping/BottomUpMapperTest.java b/src/test/java/org/cqfn/astranaut/core/algorithms/mapping/BottomUpMapperTest.java index 875796d..dd6d2e0 100644 --- a/src/test/java/org/cqfn/astranaut/core/algorithms/mapping/BottomUpMapperTest.java +++ b/src/test/java/org/cqfn/astranaut/core/algorithms/mapping/BottomUpMapperTest.java @@ -53,7 +53,9 @@ void testIdenticalTrees() { */ @Test void testOneWasRemoved() { - final Node first = LittleTrees.createStatementListWithThreeChildren(); + final Node first = LittleTrees.createStatementListWithThreeChildren( + LittleTrees.createIntegerLiteral(2) + ); final Node second = LittleTrees.createStatementListWithTwoChildren(); final Mapper mapper = new BottomUpMapper(); final Mapping mapping = mapper.map(first, second); diff --git a/src/test/java/org/cqfn/astranaut/core/example/LittleTrees.java b/src/test/java/org/cqfn/astranaut/core/example/LittleTrees.java index 86d2ed0..beedf33 100644 --- a/src/test/java/org/cqfn/astranaut/core/example/LittleTrees.java +++ b/src/test/java/org/cqfn/astranaut/core/example/LittleTrees.java @@ -162,9 +162,10 @@ public static Node createStatementListWithTwoChildren() { /** * Creates a tree (statement list) that has three children. + * @param assignable Node whose value is assigned in the third (middle) statement * @return Root node */ - public static Node createStatementListWithThreeChildren() { + public static Node createStatementListWithThreeChildren(final Node assignable) { return createStatementBlock( wrapExpressionWithStatement( createAssignment( @@ -175,7 +176,7 @@ public static Node createStatementListWithThreeChildren() { wrapExpressionWithStatement( createAssignment( createVariable("y"), - createIntegerLiteral(2) + assignable ) ), createReturnStatement( @@ -184,6 +185,36 @@ public static Node createStatementListWithThreeChildren() { ); } + /** + * Creates a tree that has a "replace" action in it. + * @return Root node + */ + public static DifferenceNode createTreeWithReplaceAction() { + final Node before = createIntegerLiteral(2); + final Node after = createVariable("x"); + final DifferenceTreeBuilder builder = new DifferenceTreeBuilder( + createStatementBlock( + wrapExpressionWithStatement( + createAssignment( + createVariable("x"), + createIntegerLiteral(1) + ) + ), + wrapExpressionWithStatement( + createAssignment( + createVariable("y"), + before + ) + ), + createReturnStatement( + createVariable("x") + ) + ) + ); + builder.replaceNode(before, after); + return builder.getRoot(); + } + /** * Creates a tree that has a "delete" action in it. * @return Root node