From 816c847836c0a09575dad4c08a53fef2ffb3b6e7 Mon Sep 17 00:00:00 2001 From: Ivan Kniazkov Date: Fri, 9 Feb 2024 11:41:08 +0300 Subject: [PATCH 1/6] Mapping algorithm --- .../algorithms/mapping/BottomUpMapper.java | 42 ++++ .../mapping/BottomUpMappingAlgorithm.java | 185 ++++++++++++++++++ .../algorithms/mapping/TopDownMapper.java | 83 -------- ...apperTest.java => BottomUpMapperTest.java} | 6 +- 4 files changed, 230 insertions(+), 86 deletions(-) create mode 100644 src/main/java/org/cqfn/astranaut/core/algorithms/mapping/BottomUpMapper.java create mode 100644 src/main/java/org/cqfn/astranaut/core/algorithms/mapping/BottomUpMappingAlgorithm.java delete mode 100644 src/main/java/org/cqfn/astranaut/core/algorithms/mapping/TopDownMapper.java rename src/test/java/org/cqfn/astranaut/core/algorithms/mapping/{TopDownMapperTest.java => BottomUpMapperTest.java} (93%) diff --git a/src/main/java/org/cqfn/astranaut/core/algorithms/mapping/BottomUpMapper.java b/src/main/java/org/cqfn/astranaut/core/algorithms/mapping/BottomUpMapper.java new file mode 100644 index 0000000..4eba07a --- /dev/null +++ b/src/main/java/org/cqfn/astranaut/core/algorithms/mapping/BottomUpMapper.java @@ -0,0 +1,42 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2024 Ivan Kniazkov + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package org.cqfn.astranaut.core.algorithms.mapping; + +import org.cqfn.astranaut.core.Node; + +/** + * Bottom-up mapper. + * Tries to match leaf nodes first, and then subtrees containing leaf nodes, + * gradually increasing the size of the matched subtrees. + * + * @since 1.1.0 + */ +public final class BottomUpMapper implements Mapper { + @Override + public Mapping map(final Node left, final Node right) { + final BottomUpMappingAlgorithm algorithm = new BottomUpMappingAlgorithm(left, right); + algorithm.execute(); + return algorithm.getResult(); + } +} 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 new file mode 100644 index 0000000..86dbd43 --- /dev/null +++ b/src/main/java/org/cqfn/astranaut/core/algorithms/mapping/BottomUpMappingAlgorithm.java @@ -0,0 +1,185 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2024 Ivan Kniazkov + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package org.cqfn.astranaut.core.algorithms.mapping; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import org.cqfn.astranaut.core.Node; +import org.cqfn.astranaut.core.algorithms.Depth; +import org.cqfn.astranaut.core.algorithms.hash.AbsoluteHash; +import org.cqfn.astranaut.core.algorithms.hash.Hash; + +/** + * Bottom-up mapping algorithm. + * Tries to match leaf nodes first, and then subtrees containing leaf nodes, + * gradually increasing the size of the matched subtrees. + * + * @since 1.1.0 + */ +class BottomUpMappingAlgorithm { + /** + * Set of node hashes. + */ + private final Hash hash; + + /** + * Set of node depths. + */ + private final Depth depth; + + /** + * Not yet processed nodes from the 'left' tree. + */ + private final Set left; + + /** + * Not yet processed nodes from the 'right' tree. + */ + private final Set right; + + /** + * Left-to-right mapping. + */ + private final Map ltr; + + /** + * Right-to-left mapping. + */ + private final Map rtl; + + + /** + * Constructor. + * @param left Root node of the 'left' tree + * @param right Root node of the 'right' tree + */ + BottomUpMappingAlgorithm(final Node left, final Node right) { + this.hash = new AbsoluteHash(); + this.depth = new Depth(); + this.left = this.createNodeSet(left); + this.right = this.createNodeSet(right); + this.ltr = new HashMap<>(); + this.rtl = new HashMap<>(); + } + + /** + * Performs the mapping. + */ + void execute() { + + } + + /** + * Returns result of mapping. + * @return Result of mapping + */ + Mapping getResult() { + return new Mapping() { + @Override + public Node getRight(final Node left) { + return BottomUpMappingAlgorithm.this.ltr.get(left); + } + + @Override + public Node getLeft(Node right) { + return BottomUpMappingAlgorithm.this.rtl.get(right); + } + }; + } + + /** + * Creates an initial set of nodes suitable for processing from the tree. + * @param root The root of the tree + * @return Set of extended nodes + */ + private Set createNodeSet(final Node root) { + final Set set = new HashSet<>(); + this.createNodeSet(root, null, set); + return set; + } + + /** + * Creates an initial set of nodes suitable for processing from the tree (recursive method). + * @param node The current node + * @param parent The current node parent + * @param set The resulting set + */ + private void createNodeSet(final Node node, final Node parent, final Set set) { + set.add(new ExtNode(node, parent)); + node.forEachChild(child -> BottomUpMappingAlgorithm.this.createNodeSet(child, node, set)); + } + + /** + * Extended node containing information required for mapping. + * + * @since 1.1.0 + */ + private class ExtNode { + /** + * The node itself. + */ + private final Node node; + + /** + * The parent of the node. + */ + private final Node parent; + + /** + * Constructor. + * @param node The node itself + * @param parent The parent of the node + */ + ExtNode(final Node node, final Node parent) { + this.node = node; + this.parent = parent; + } + + Node getNode() { + return this.node; + } + + Node getParent() { + return this.parent; + } + + /** + * Calculates the hash of the node. + * @return Node hash + */ + int getHash() { + return BottomUpMappingAlgorithm.this.hash.calculate(this.node); + } + + /** + * Calculates the depth of the node. + * @return Node depth + */ + int getDepth() { + return BottomUpMappingAlgorithm.this.depth.calculate(this.node); + } + } +} diff --git a/src/main/java/org/cqfn/astranaut/core/algorithms/mapping/TopDownMapper.java b/src/main/java/org/cqfn/astranaut/core/algorithms/mapping/TopDownMapper.java deleted file mode 100644 index 5541a43..0000000 --- a/src/main/java/org/cqfn/astranaut/core/algorithms/mapping/TopDownMapper.java +++ /dev/null @@ -1,83 +0,0 @@ -/* - * The MIT License (MIT) - * - * Copyright (c) 2024 Ivan Kniazkov - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included - * in all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ -package org.cqfn.astranaut.core.algorithms.mapping; - -import java.util.HashMap; -import java.util.Map; -import org.cqfn.astranaut.core.Node; - -/** - * Top-down mapping algorithm. - * It starts comparing trees starting from the roots and sequentially 'descends down'. - * - * @since 1.1.0 - */ -public final class TopDownMapper implements Mapper { - /** - * Left-to-right mapping. - */ - private final Map ltr; - - /** - * Right-to-left mapping. - */ - private final Map rtl; - - /** - * Constructor. - */ - public TopDownMapper() { - this.ltr = new HashMap<>(); - this.rtl = new HashMap<>(); - } - - @Override - public Mapping map(final Node left, final Node right) { - this.compareTwoNodes(left, right); - return new Mapping() { - @Override - public Node getRight(final Node left) { - return TopDownMapper.this.ltr.get(left); - } - - @Override - public Node getLeft(final Node right) { - return TopDownMapper.this.rtl.get(right); - } - }; - } - - /** - * Compares two nodes and updates the mapping structures if the nodes are matched. - * @param left Left node - * @param right Right node - */ - private void compareTwoNodes(final Node left, final Node right) { - if (left.getTypeName().equals(right.getTypeName()) - && left.getData().equals(right.getData())) { - this.ltr.put(left, right); - this.rtl.put(right, left); - } - } -} diff --git a/src/test/java/org/cqfn/astranaut/core/algorithms/mapping/TopDownMapperTest.java b/src/test/java/org/cqfn/astranaut/core/algorithms/mapping/BottomUpMapperTest.java similarity index 93% rename from src/test/java/org/cqfn/astranaut/core/algorithms/mapping/TopDownMapperTest.java rename to src/test/java/org/cqfn/astranaut/core/algorithms/mapping/BottomUpMapperTest.java index 80cdbc7..5261859 100644 --- a/src/test/java/org/cqfn/astranaut/core/algorithms/mapping/TopDownMapperTest.java +++ b/src/test/java/org/cqfn/astranaut/core/algorithms/mapping/BottomUpMapperTest.java @@ -29,11 +29,11 @@ import org.junit.jupiter.api.Test; /** - * Test for {@link TopDownMapper} class. + * Test for {@link BottomUpMapper} class. * * @since 1.0 */ -class TopDownMapperTest { +class BottomUpMapperTest { /** * A test. */ @@ -41,7 +41,7 @@ class TopDownMapperTest { void test() { final Node first = LittleTrees.createTreeWithDeleteAction(); final Node second = LittleTrees.createTreeWithDeleteAction(); - final Mapper mapper = new TopDownMapper(); + final Mapper mapper = new BottomUpMapper(); final Mapping mapping = mapper.map(first, second); Assertions.assertEquals(mapping.getRight(first), second); Assertions.assertEquals(mapping.getLeft(second), first); From 67117d7e5a644e0adb210acfdd15b678af3f5186 Mon Sep 17 00:00:00 2001 From: Ivan Kniazkov Date: Wed, 14 Feb 2024 15:33:02 +0300 Subject: [PATCH 2/6] Mapping by identical hashes from bottom to top --- .../mapping/BottomUpMappingAlgorithm.java | 151 +++++++++++------- 1 file changed, 94 insertions(+), 57 deletions(-) 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 86dbd43..094e68c 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 @@ -23,10 +23,13 @@ */ package org.cqfn.astranaut.core.algorithms.mapping; +import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; +import java.util.List; import java.util.Map; import java.util.Set; +import java.util.TreeMap; import org.cqfn.astranaut.core.Node; import org.cqfn.astranaut.core.algorithms.Depth; import org.cqfn.astranaut.core.algorithms.hash.AbsoluteHash; @@ -43,22 +46,27 @@ class BottomUpMappingAlgorithm { /** * Set of node hashes. */ - private final Hash hash; + private final Hash hashes; /** * Set of node depths. */ private final Depth depth; + /** + * Relationship of the nodes to their parents. + */ + private final Map parents; + /** * Not yet processed nodes from the 'left' tree. */ - private final Set left; + private final Set left; /** * Not yet processed nodes from the 'right' tree. */ - private final Set right; + private final Set right; /** * Left-to-right mapping. @@ -70,15 +78,15 @@ class BottomUpMappingAlgorithm { */ private final Map rtl; - /** * Constructor. * @param left Root node of the 'left' tree * @param right Root node of the 'right' tree */ BottomUpMappingAlgorithm(final Node left, final Node right) { - this.hash = new AbsoluteHash(); + this.hashes = new AbsoluteHash(); this.depth = new Depth(); + this.parents = new HashMap<>(); this.left = this.createNodeSet(left); this.right = this.createNodeSet(right); this.ltr = new HashMap<>(); @@ -89,7 +97,8 @@ class BottomUpMappingAlgorithm { * Performs the mapping. */ void execute() { - + final Map> draft = this.performInitialMapping(); + this.absorbLargestSubtrees(draft); } /** @@ -99,13 +108,13 @@ void execute() { Mapping getResult() { return new Mapping() { @Override - public Node getRight(final Node left) { - return BottomUpMappingAlgorithm.this.ltr.get(left); + public Node getRight(final Node node) { + return BottomUpMappingAlgorithm.this.ltr.get(node); } @Override - public Node getLeft(Node right) { - return BottomUpMappingAlgorithm.this.rtl.get(right); + public Node getLeft(final Node node) { + return BottomUpMappingAlgorithm.this.rtl.get(node); } }; } @@ -113,10 +122,10 @@ public Node getLeft(Node right) { /** * Creates an initial set of nodes suitable for processing from the tree. * @param root The root of the tree - * @return Set of extended nodes + * @return Set of nodes */ - private Set createNodeSet(final Node root) { - final Set set = new HashSet<>(); + private Set createNodeSet(final Node root) { + final Set set = new HashSet<>(); this.createNodeSet(root, null, set); return set; } @@ -127,59 +136,87 @@ private Set createNodeSet(final Node root) { * @param parent The current node parent * @param set The resulting set */ - private void createNodeSet(final Node node, final Node parent, final Set set) { - set.add(new ExtNode(node, parent)); - node.forEachChild(child -> BottomUpMappingAlgorithm.this.createNodeSet(child, node, set)); + private void createNodeSet(final Node node, final Node parent, final Set set) { + set.add(node); + this.parents.put(node, parent); + node.forEachChild(child -> this.createNodeSet(child, node, set)); } /** - * Extended node containing information required for mapping. - * - * @since 1.1.0 - */ - private class ExtNode { - /** - * The node itself. - */ - private final Node node; - - /** - * The parent of the node. - */ - private final Node parent; - - /** - * Constructor. - * @param node The node itself - * @param parent The parent of the node - */ - ExtNode(final Node node, final Node parent) { - this.node = node; - this.parent = parent; - } - - Node getNode() { - return this.node; + * Performs hash calculation of nodes from the 'right' set. + * @return The hash relation to the list of nodes that have such a hash + */ + private Map> calculateRightHashes() { + final Map> result = new TreeMap<>(); + for (final Node node : this.right) { + final int hash = this.hashes.calculate(node); + final List list = + result.computeIfAbsent(hash, k -> new ArrayList<>(1)); + list.add(node); } + return result; + } - Node getParent() { - return this.parent; + /** + * Performs initial (draft) node mapping. + * @return Relationships of nodes to lists of nodes to which they can be mapped to + */ + private Map> performInitialMapping() { + final Map> result = new HashMap<>(); + final Map> relation = this.calculateRightHashes(); + for (final Node node : this.left) { + final int hash = this.hashes.calculate(node); + final List list = relation.get(hash); + if (list != null) { + result.put(node, list); + } } + return result; + } - /** - * Calculates the hash of the node. - * @return Node hash - */ - int getHash() { - return BottomUpMappingAlgorithm.this.hash.calculate(this.node); + /** + * Selects the largest size subtrees from the initial node relation and maps them. + * @param draft Initial node relation + */ + private void absorbLargestSubtrees(final Map> draft) { + final List sorted = new ArrayList<>(draft.keySet()); + sorted.sort( + (first, second) -> -Integer.compare( + this.depth.calculate(first), + this.depth.calculate(second) + ) + ); + for (final Node node : sorted) { + final List related = draft.get(node); + if (related != null && related.size() == 1 && !this.ltr.containsKey(node)) { + this.mapSubtreesWithTheSameHash(node, related.get(0), draft); + } + if (draft.isEmpty()) { + break; + } } + } - /** - * Calculates the depth of the node. - * @return Node depth - */ - int getDepth() { - return BottomUpMappingAlgorithm.this.depth.calculate(this.node); + /** + * Maps subtrees with the same hash, adding the corresponding nodes to the resulting + * collections and removing them from the initial mapping. + * @param node Left node + * @param related Related node to the left node + * @param draft Initial node relation + */ + private void mapSubtreesWithTheSameHash( + final Node node, + final Node related, + final Map> draft) { + assert this.hashes.calculate(node) == this.hashes.calculate(related); + draft.remove(node); + 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 first = node.getChild(index); + final Node second = related.getChild(index); + this.mapSubtreesWithTheSameHash(first, second, draft); } } } From 17568b710a06f02705b9ffdf2640af1a164c5219 Mon Sep 17 00:00:00 2001 From: Ivan Kniazkov Date: Wed, 14 Feb 2024 15:53:30 +0300 Subject: [PATCH 3/6] Testing mapping with one deleted node --- .../mapping/BottomUpMapperTest.java | 23 +++++++++- .../astranaut/core/example/LittleTrees.java | 42 +++++++++++++++++++ 2 files changed, 63 insertions(+), 2 deletions(-) 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 5261859..d2d19cf 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 @@ -35,10 +35,10 @@ */ class BottomUpMapperTest { /** - * A test. + * Test in which the identical trees are mapped. */ @Test - void test() { + void testIdenticalTrees() { final Node first = LittleTrees.createTreeWithDeleteAction(); final Node second = LittleTrees.createTreeWithDeleteAction(); final Mapper mapper = new BottomUpMapper(); @@ -46,4 +46,23 @@ void test() { Assertions.assertEquals(mapping.getRight(first), second); Assertions.assertEquals(mapping.getLeft(second), first); } + + /** + * Testing mapping of two trees, with some node removed in the second tree. + */ + @Test + void testOneWasRemoved() { + final Node first = LittleTrees.createStatementListWithThreeChildren(); + final Node second = LittleTrees.createStatementListWithTwoChildren(); + final Mapper mapper = new BottomUpMapper(); + final Mapping mapping = mapper.map(first, second); + Node left = first.getChild(0); + Node right = second.getChild(0); + Assertions.assertEquals(mapping.getRight(left), right); + Assertions.assertEquals(mapping.getLeft(right), left); + left = first.getChild(2); + right = second.getChild(1); + Assertions.assertEquals(mapping.getRight(left), right); + Assertions.assertEquals(mapping.getLeft(right), left); + } } 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 72e083c..07aeb08 100644 --- a/src/test/java/org/cqfn/astranaut/core/example/LittleTrees.java +++ b/src/test/java/org/cqfn/astranaut/core/example/LittleTrees.java @@ -142,6 +142,48 @@ public static Node createStatementBlock(final Node... statements) { return result; } + /** + * Creates a tree (statement list) that has two children. + * @return Root node + */ + public static Node createStatementListWithTwoChildren() { + return createStatementBlock( + wrapExpressionWithStatement( + createAssignment( + createVariable("x"), + createIntegerLiteral(1) + ) + ), + createReturnStatement( + createVariable("x") + ) + ); + } + + /** + * Creates a tree (statement list) that has three children. + * @return Root node + */ + public static Node createStatementListWithThreeChildren() { + return createStatementBlock( + wrapExpressionWithStatement( + createAssignment( + createVariable("x"), + createIntegerLiteral(1) + ) + ), + wrapExpressionWithStatement( + createAssignment( + createVariable("y"), + createIntegerLiteral(2) + ) + ), + createReturnStatement( + createVariable("x") + ) + ); + } + /** * Creates a tree that has a "delete" action in it. * @return Root node From fbf20ac4b28ec71ff600e899291e3820144888ba Mon Sep 17 00:00:00 2001 From: Ivan Kniazkov Date: Wed, 14 Feb 2024 17:01:07 +0300 Subject: [PATCH 4/6] Mapping partially mapped nodes --- .../mapping/BottomUpMappingAlgorithm.java | 66 +++++++++++++++++++ .../mapping/BottomUpMapperTest.java | 2 + 2 files changed, 68 insertions(+) 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 094e68c..b19ba08 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 @@ -26,6 +26,7 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; +import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; @@ -99,6 +100,13 @@ class BottomUpMappingAlgorithm { void execute() { final Map> draft = this.performInitialMapping(); this.absorbLargestSubtrees(draft); + Node node = this.findPartiallyMappedLeftNode(); + while (node != null) { + node = this.mapPartiallyMappedLeftNode(node); + if (node == null) { + node = this.findPartiallyMappedLeftNode(); + } + } } /** @@ -210,6 +218,8 @@ private void mapSubtreesWithTheSameHash( final Map> draft) { assert this.hashes.calculate(node) == this.hashes.calculate(related); draft.remove(node); + this.left.remove(node); + this.right.remove(related); this.ltr.put(node, related); this.rtl.put(related, node); final int count = node.getChildCount(); @@ -219,4 +229,60 @@ private void mapSubtreesWithTheSameHash( this.mapSubtreesWithTheSameHash(first, second, draft); } } + + /** + * Finds a partially mapped node from the 'left' set, that is, one that has some children + * mapped and some not. + * @return A node or {@code null} if such node not found + */ + private Node findPartiallyMappedLeftNode() { + Node result = null; + final Iterator iterator = this.left.iterator(); + while (result == null && iterator.hasNext()) { + final Node node = iterator.next(); + 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)) { + result = node; + break; + } + } + } + return result; + } + + /** + * Tries to map a partially mapped 'left' node to another node. + * @param node A node to be mapped + * @return Next partially mapped node to be processed + */ + private Node mapPartiallyMappedLeftNode(final Node node) { + final Set ancestors = new HashSet<>(); + Node next = null; + node.forEachChild( + child -> { + final Node mapped = this.ltr.get(child); + if (mapped != null) { + ancestors.add(this.parents.get(mapped)); + } + } + ); + do { + if (ancestors.size() != 1) { + break; + } + final Node related = ancestors.iterator().next(); + if (!node.getTypeName().equals(related.getTypeName()) + || !node.getData().equals(related.getData())) { + break; + } + this.right.remove(related); + this.ltr.put(node, related); + this.rtl.put(related, node); + next = this.parents.get(node); + } while (false); + this.left.remove(node); + return next; + } } 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 d2d19cf..ada811b 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 @@ -56,6 +56,8 @@ void testOneWasRemoved() { final Node second = LittleTrees.createStatementListWithTwoChildren(); final Mapper mapper = new BottomUpMapper(); final Mapping mapping = mapper.map(first, second); + Assertions.assertEquals(mapping.getRight(first), second); + Assertions.assertEquals(mapping.getLeft(second), first); Node left = first.getChild(0); Node right = second.getChild(0); Assertions.assertEquals(mapping.getRight(left), right); From 9c51dbd9ac00ad9dd15e0d560f18f2e8cd8630c6 Mon Sep 17 00:00:00 2001 From: Ivan Kniazkov Date: Wed, 14 Feb 2024 17:18:49 +0300 Subject: [PATCH 5/6] Mapping: create list of deleted nodes --- .../mapping/BottomUpMappingAlgorithm.java | 19 +++++++++++++++++++ .../core/algorithms/mapping/Mapping.java | 8 ++++++++ .../mapping/BottomUpMapperTest.java | 4 ++++ 3 files changed, 31 insertions(+) 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 b19ba08..3e70db3 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 @@ -24,6 +24,7 @@ package org.cqfn.astranaut.core.algorithms.mapping; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; @@ -79,6 +80,11 @@ class BottomUpMappingAlgorithm { */ private final Map rtl; + /** + * Set of deleted nodes. + */ + private final Set deleted; + /** * Constructor. * @param left Root node of the 'left' tree @@ -92,6 +98,7 @@ class BottomUpMappingAlgorithm { this.right = this.createNodeSet(right); this.ltr = new HashMap<>(); this.rtl = new HashMap<>(); + this.deleted = new HashSet<>(); } /** @@ -124,6 +131,11 @@ public Node getRight(final Node node) { public Node getLeft(final Node node) { return BottomUpMappingAlgorithm.this.rtl.get(node); } + + @Override + public Set getDeleted() { + return Collections.unmodifiableSet(BottomUpMappingAlgorithm.this.deleted); + } }; } @@ -280,6 +292,13 @@ 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); + } + } next = this.parents.get(node); } while (false); this.left.remove(node); 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 e53f7e7..2b3fc70 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.Set; import org.cqfn.astranaut.core.Node; /** @@ -47,4 +48,11 @@ public interface Mapping { * {@code null} if there is nothing corresponding to the node of the 'right' tree */ Node getLeft(Node right); + + /** + * Returns the set of nodes of the 'left' tree that need to be removed + * to get the 'right' tree. + * @return The set of deleted nodes + */ + Set getDeleted(); } 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 ada811b..875796d 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 @@ -23,6 +23,7 @@ */ package org.cqfn.astranaut.core.algorithms.mapping; +import java.util.Set; import org.cqfn.astranaut.core.Node; import org.cqfn.astranaut.core.example.LittleTrees; import org.junit.jupiter.api.Assertions; @@ -66,5 +67,8 @@ void testOneWasRemoved() { right = second.getChild(1); Assertions.assertEquals(mapping.getRight(left), right); Assertions.assertEquals(mapping.getLeft(right), left); + final Set deleted = mapping.getDeleted(); + Assertions.assertEquals(1, deleted.size()); + Assertions.assertEquals(first.getChild(1), deleted.iterator().next()); } } From e213ef5bb59c80173495ee588d63efcc10cbd9f2 Mon Sep 17 00:00:00 2001 From: Ivan Kniazkov Date: Thu, 15 Feb 2024 08:47:43 +0300 Subject: [PATCH 6/6] Added and tested the main 'build()' method for difference tree builder --- .../algorithms/DifferenceTreeBuilder.java | 23 +++++++- .../algorithms/DifferenceTreeBuilderTest.java | 55 +++++++++++++++++++ 2 files changed, 75 insertions(+), 3 deletions(-) create mode 100644 src/test/java/org/cqfn/astranaut/core/algorithms/DifferenceTreeBuilderTest.java 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 ca995bf..3091bda 100644 --- a/src/main/java/org/cqfn/astranaut/core/algorithms/DifferenceTreeBuilder.java +++ b/src/main/java/org/cqfn/astranaut/core/algorithms/DifferenceTreeBuilder.java @@ -27,6 +27,8 @@ import java.util.Map; import org.cqfn.astranaut.core.DifferenceNode; import org.cqfn.astranaut.core.Node; +import org.cqfn.astranaut.core.algorithms.mapping.Mapper; +import org.cqfn.astranaut.core.algorithms.mapping.Mapping; /** * Builder of difference syntax tree, that is, one that stores changes between two trees. @@ -48,13 +50,28 @@ public final class DifferenceTreeBuilder { /** * Constructor. - * @param prototype Root node of an 'ordinary', non-difference original tree before the changes + * @param before Root node of an 'ordinary', non-difference original tree before the changes */ - public DifferenceTreeBuilder(final Node prototype) { - this.root = new DifferenceNode(prototype); + public DifferenceTreeBuilder(final Node before) { + this.root = new DifferenceNode(before); this.parents = DifferenceTreeBuilder.buildParentsMap(this.root); } + /** + * Builds a difference tree based on the original tree and the tree after changes. + * @param after Root node of tree before the changes + * @param mapper A mapper used for node mappings + * @return Result of operation, {@code true} if difference tree was built + */ + public boolean build(final Node after, final Mapper mapper) { + final Mapping mapping = mapper.map(this.root.getPrototype(), after); + boolean result = true; + for (final Node deleted : mapping.getDeleted()) { + result = result & this.deleteNode(deleted); + } + return result; + } + /** * Returns root of resulting difference tree. * @return Root node of difference 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 new file mode 100644 index 0000000..b5460c9 --- /dev/null +++ b/src/test/java/org/cqfn/astranaut/core/algorithms/DifferenceTreeBuilderTest.java @@ -0,0 +1,55 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2024 Ivan Kniazkov + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package org.cqfn.astranaut.core.algorithms; + +import org.cqfn.astranaut.core.DifferenceNode; +import org.cqfn.astranaut.core.Node; +import org.cqfn.astranaut.core.algorithms.mapping.BottomUpMapper; +import org.cqfn.astranaut.core.example.LittleTrees; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +/** + * Testing {@link DifferenceTreeBuilder} class. + * + * @since 1.1.0 + */ +class DifferenceTreeBuilderTest { + /** + * Testing the construction of a difference tree with a deleted node. + */ + @Test + void testTreeWithDeletedNode() { + final Node before = LittleTrees.createStatementListWithThreeChildren(); + final Node after = LittleTrees.createStatementListWithTwoChildren(); + 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.createTreeWithDeleteAction(); + Assertions.assertTrue(expected.deepCompare(diff)); + Assertions.assertTrue(before.deepCompare(diff.getBefore())); + Assertions.assertTrue(after.deepCompare(diff.getAfter())); + } +}