From 18c7f4360037b8ec7afad237fb847ee93f18d9a2 Mon Sep 17 00:00:00 2001 From: Ivan Kniazkov Date: Wed, 28 Feb 2024 14:55:00 +0300 Subject: [PATCH] Insertion parsing + tests --- .../org/cqfn/astranaut/core/Insertion.java | 93 +++++++++++++++ .../algorithms/DifferenceTreeBuilder.java | 109 ++++++++++++++---- .../algorithms/mapping/BottomUpAlgorithm.java | 39 ++++++- .../core/algorithms/mapping/Mapping.java | 7 ++ .../core/utils/deserializer/ActionList.java | 16 +-- .../utils/deserializer/NodeDescriptor.java | 2 +- .../algorithms/DifferenceTreeBuilderTest.java | 19 +++ .../astranaut/core/example/LittleTrees.java | 31 +++++ 8 files changed, 283 insertions(+), 33 deletions(-) create mode 100644 src/main/java/org/cqfn/astranaut/core/Insertion.java diff --git a/src/main/java/org/cqfn/astranaut/core/Insertion.java b/src/main/java/org/cqfn/astranaut/core/Insertion.java new file mode 100644 index 0000000..5d0697c --- /dev/null +++ b/src/main/java/org/cqfn/astranaut/core/Insertion.java @@ -0,0 +1,93 @@ +/* + * 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; + +import java.util.Objects; + +/** + * This class contains information about the node being inserted as a child of another node. + * + * @since 1.1.0 + */ +public final class Insertion { + /** + * Node being inserted. + */ + private final Node inserted; + + /** + * Parent node into which the child node will be inserted. + */ + private final Node into; + + /** + * Child node after which to insert. + */ + private final Node after; + + /** + * Constructor. + * @param inserted Node being inserted + * @param into Parent node into which the child node will be inserted + * @param after Child node after which to insert + */ + public Insertion(final Node inserted, final Node into, final Node after) { + this.inserted = Objects.requireNonNull(inserted); + this.into = into; + this.after = after; + } + + /** + * Another constructor. + * @param inserted Node being inserted + * @param after Child node after which to insert + */ + public Insertion(final Node inserted, final Node after) { + this(inserted, null, Objects.requireNonNull(after)); + } + + /** + * Returns node being inserted. + * @return A node + */ + public Node getNode() { + return this.inserted; + } + + /** + * Returns parent node into which the child node will be inserted. + * @return A node + */ + public Node getInto() { + return this.into; + } + + /** + * Returns child node after which to insert. + * @return A node + */ + public Node getAfter() { + return this.after; + } +} 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 25e1bc1..cb8ad52 100644 --- a/src/main/java/org/cqfn/astranaut/core/algorithms/DifferenceTreeBuilder.java +++ b/src/main/java/org/cqfn/astranaut/core/algorithms/DifferenceTreeBuilder.java @@ -26,6 +26,7 @@ import java.util.HashMap; import java.util.Map; import org.cqfn.astranaut.core.DifferenceNode; +import org.cqfn.astranaut.core.Insertion; import org.cqfn.astranaut.core.Node; import org.cqfn.astranaut.core.algorithms.mapping.Mapper; import org.cqfn.astranaut.core.algorithms.mapping.Mapping; @@ -37,11 +38,16 @@ */ public final class DifferenceTreeBuilder { /** - * The relationship of the nodes to their parents. - * This information is necessary to implement algorithms for - * adding, removing and replacing nodes. + * Default node info (to avoid null checks). */ - private final Map parents; + private static final NodeInfo DEFAULT_INFO = new NodeInfo(null, null); + + /** + * The relationship of the nodes to their parents and corresponding difference nodes. + * This information is necessary to implement algorithms for inserting, removing + * and replacing nodes. + */ + private final Map info; /** * Root node. @@ -54,7 +60,7 @@ public final class DifferenceTreeBuilder { */ public DifferenceTreeBuilder(final Node before) { this.root = new DifferenceNode(before); - this.parents = DifferenceTreeBuilder.buildParentsMap(this.root); + this.info = DifferenceTreeBuilder.buildNodeInfoMap(this.root); } /** @@ -66,6 +72,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 Insertion insertion : mapping.getInserted()) { + result = result & this.insertNode(insertion); + } for (final Map.Entry replaced : mapping.getReplaced().entrySet()) { result = result & this.replaceNode(replaced.getKey(), replaced.getValue()); } @@ -86,15 +95,23 @@ public DifferenceNode getRoot() { /** * Adds an action to the difference tree that inserts a node after another node. * If no other node is specified, inserts at the beginning of the children's list. - * @param node Child element that will be inserted - * @param after Node after which to insert + * @param insertion Full information about the node being inserted * @return Result of operation, {@code true} if action was added */ - public boolean insertNodeAfter(final Node node, final Node after) { + public boolean insertNode(final Insertion insertion) { boolean result = false; - final DifferenceNode parent = this.parents.get(after); + DifferenceNode parent = this.info.getOrDefault( + insertion.getInto(), + DifferenceTreeBuilder.DEFAULT_INFO + ).getDiff(); + if (parent == null) { + parent = this.info.getOrDefault( + insertion.getAfter(), + DifferenceTreeBuilder.DEFAULT_INFO + ).getParent(); + } if (parent != null) { - result = parent.insertNodeAfter(node, after); + result = parent.insertNodeAfter(insertion.getNode(), insertion.getAfter()); } return result; } @@ -107,7 +124,8 @@ public boolean insertNodeAfter(final Node node, final Node after) { */ public boolean replaceNode(final Node node, final Node replacement) { boolean result = false; - final DifferenceNode parent = this.parents.get(node); + final DifferenceNode parent = + this.info.getOrDefault(node, DifferenceTreeBuilder.DEFAULT_INFO).getParent(); if (parent != null) { result = parent.replaceNode(node, replacement); } @@ -121,7 +139,8 @@ public boolean replaceNode(final Node node, final Node replacement) { */ public boolean deleteNode(final Node node) { boolean result = false; - final DifferenceNode parent = this.parents.get(node); + final DifferenceNode parent = + this.info.getOrDefault(node, DifferenceTreeBuilder.DEFAULT_INFO).getParent(); if (parent != null) { result = parent.deleteNode(node); } @@ -133,28 +152,72 @@ public boolean deleteNode(final Node node) { * @param root Root node * @return The map containing relationship of the nodes to their parents. */ - private static Map buildParentsMap(final DifferenceNode root) { - final Map map = new HashMap<>(); - DifferenceTreeBuilder.buildParentsMap(map, root); + private static Map buildNodeInfoMap(final DifferenceNode root) { + final Map map = new HashMap<>(); + map.put(root.getPrototype(), new NodeInfo(root, null)); + DifferenceTreeBuilder.buildNodeInfoMap(map, root); return map; } /** * Builds the map containing relationship of the nodes to their parents (recursive method). * @param map Where to put the results - * @param node Current node + * @param parent Parent node */ - private static void buildParentsMap( - final Map map, - final DifferenceNode node) { - node.forEachChild( + private static void buildNodeInfoMap( + final Map map, + final DifferenceNode parent) { + parent.forEachChild( child -> { if (child instanceof DifferenceNode) { - final DifferenceNode diff = (DifferenceNode) child; - map.put(diff.getPrototype(), node); - DifferenceTreeBuilder.buildParentsMap(map, diff); + final DifferenceNode node = (DifferenceNode) child; + map.put(node.getPrototype(), new NodeInfo(node, parent)); + DifferenceTreeBuilder.buildNodeInfoMap(map, node); } } ); } + + /** + * Some additional information about each node needed to insert, replace, or delete nodes. + * + * @since 1.1.0 + */ + private static final class NodeInfo { + /** + * The corresponding difference node. + */ + private final DifferenceNode diff; + + /** + * The parent node. + */ + private final DifferenceNode parent; + + /** + * Constructor. + * @param diff The corresponding difference node + * @param parent The parent node + */ + NodeInfo(final DifferenceNode diff, final DifferenceNode parent) { + this.diff = diff; + this.parent = parent; + } + + /** + * Returns corresponding difference node. + * @return Difference node + */ + public DifferenceNode getDiff() { + return this.diff; + } + + /** + * Returns parent node. + * @return Difference node containing this node + */ + public DifferenceNode getParent() { + return this.parent; + } + } } diff --git a/src/main/java/org/cqfn/astranaut/core/algorithms/mapping/BottomUpAlgorithm.java b/src/main/java/org/cqfn/astranaut/core/algorithms/mapping/BottomUpAlgorithm.java index 6dafa8f..bace428 100644 --- a/src/main/java/org/cqfn/astranaut/core/algorithms/mapping/BottomUpAlgorithm.java +++ b/src/main/java/org/cqfn/astranaut/core/algorithms/mapping/BottomUpAlgorithm.java @@ -33,6 +33,7 @@ import java.util.Map; import java.util.Set; import java.util.TreeMap; +import org.cqfn.astranaut.core.Insertion; import org.cqfn.astranaut.core.Node; import org.cqfn.astranaut.core.algorithms.Depth; import org.cqfn.astranaut.core.algorithms.hash.AbsoluteHash; @@ -87,6 +88,11 @@ class BottomUpAlgorithm { */ private final Map rtl; + /** + * Set containing inserted nodes. + */ + private final Set inserted; + /** * Map containing replaces nodes. */ @@ -111,6 +117,7 @@ class BottomUpAlgorithm { this.right = this.createNodeList(right); this.ltr = new HashMap<>(); this.rtl = new HashMap<>(); + this.inserted = new HashSet<>(); this.replaced = new HashMap<>(); this.deleted = new HashSet<>(); } @@ -309,13 +316,36 @@ private Node mapPartiallyMappedLeftNode(final Node node) { */ private void mapChildren(final Node before, final Node after) { final int sign = Integer.compare(before.getChildCount(), after.getChildCount()); - if (sign > 0) { + if (sign < 0) { + this.mapChildrenIfInserted(before, after); + } else if (sign > 0) { this.mapChildrenIfDeleted(before); - } else if (sign == 0) { + } else { this.mapChildrenIfReplaced(before, after); } } + /** + * Maps the child nodes of partially mapped nodes if the node before changes + * has less child nodes than the node after changes, i.e., when it is obvious + * that some nodes have been inserted. + * @param before Node before changes + * @param after Node after changes + */ + private void mapChildrenIfInserted(final Node before, final Node after) { + final int count = after.getChildCount(); + Node previous = null; + for (int index = 0; index < count; index = index + 1) { + final Node child = after.getChild(index); + if (this.rtl.containsKey(child)) { + previous = this.rtl.get(child); + } else { + this.inserted.add(new Insertion(child, before, previous)); + this.unprocessed.remove(child); + } + } + } + /** * 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 @@ -383,6 +413,11 @@ public Node getLeft(final Node node) { return this.data.rtl.get(node); } + @Override + public Set getInserted() { + return Collections.unmodifiableSet(this.data.inserted); + } + @Override public Map getReplaced() { return Collections.unmodifiableMap(this.data.replaced); 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 da1eb2e..d8a376e 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 @@ -25,6 +25,7 @@ import java.util.Map; import java.util.Set; +import org.cqfn.astranaut.core.Insertion; import org.cqfn.astranaut.core.Node; /** @@ -50,6 +51,12 @@ public interface Mapping { */ Node getLeft(Node right); + /** + * Returns a collection of nodes that must be added to the 'left' tree to get the 'right' tree. + * @return The set of inserted nodes + */ + Set getInserted(); + /** * Returns relationship between the nodes of the 'left' tree that have been replaced * by nodes of the 'right' tree. diff --git a/src/main/java/org/cqfn/astranaut/core/utils/deserializer/ActionList.java b/src/main/java/org/cqfn/astranaut/core/utils/deserializer/ActionList.java index f320715..37e6b26 100644 --- a/src/main/java/org/cqfn/astranaut/core/utils/deserializer/ActionList.java +++ b/src/main/java/org/cqfn/astranaut/core/utils/deserializer/ActionList.java @@ -28,6 +28,7 @@ import java.util.Map; import java.util.Set; import org.cqfn.astranaut.core.DifferenceNode; +import org.cqfn.astranaut.core.Insertion; import org.cqfn.astranaut.core.Node; import org.cqfn.astranaut.core.algorithms.DifferenceTreeBuilder; @@ -38,9 +39,9 @@ */ public class ActionList { /** - * Collection of nodes to be inserted (node -> after which to insert). + * Collection of nodes to be inserted. */ - private final Map insert; + private final Set insert; /** * Collection of nodes to be replaced (node before changes -> node after changes). @@ -56,7 +57,7 @@ public class ActionList { * Constructor. */ public ActionList() { - this.insert = new HashMap<>(); + this.insert = new HashSet<>(); this.replace = new HashMap<>(); this.delete = new HashSet<>(); } @@ -72,10 +73,11 @@ public boolean hasActions() { /** * Adds the node to the list of nodes to be inserted. * @param node Node to be inserted + * @param into Parent node into which the child node will be inserted * @param after Node after which to insert */ - public void insertNodeAfter(final Node node, final Node after) { - this.insert.put(node, after); + public void insertNodeAfter(final Node node, final Node into, final Node after) { + this.insert.add(new Insertion(node, into, after)); } /** @@ -102,8 +104,8 @@ public void deleteNode(final Node node) { */ public DifferenceNode convertTreeToDifferenceTree(final Node root) { final DifferenceTreeBuilder builder = new DifferenceTreeBuilder(root); - for (final Map.Entry pair : this.insert.entrySet()) { - builder.insertNodeAfter(pair.getKey(), pair.getValue()); + for (final Insertion insertion : this.insert) { + builder.insertNode(insertion); } for (final Map.Entry pair : this.replace.entrySet()) { builder.replaceNode(pair.getKey(), pair.getValue()); diff --git a/src/main/java/org/cqfn/astranaut/core/utils/deserializer/NodeDescriptor.java b/src/main/java/org/cqfn/astranaut/core/utils/deserializer/NodeDescriptor.java index 31af527..9d95e30 100644 --- a/src/main/java/org/cqfn/astranaut/core/utils/deserializer/NodeDescriptor.java +++ b/src/main/java/org/cqfn/astranaut/core/utils/deserializer/NodeDescriptor.java @@ -104,7 +104,7 @@ private List convertChildren(final Factory factory, final ActionList actio if (size > 0) { after = list.get(size - 1); } - actions.insertNodeAfter(node, after); + actions.insertNodeAfter(node, null, after); } else if (converted instanceof Replace) { final Replace action = (Replace) converted; final Node node = action.getBefore(); 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 993e0f2..124d8dd 100644 --- a/src/test/java/org/cqfn/astranaut/core/algorithms/DifferenceTreeBuilderTest.java +++ b/src/test/java/org/cqfn/astranaut/core/algorithms/DifferenceTreeBuilderTest.java @@ -38,6 +38,25 @@ * @since 1.1.0 */ class DifferenceTreeBuilderTest { + /** + * Testing the construction of a difference tree with an inserted node. + */ + @Test + void testTreeWithInsertedNode() { + final Node before = LittleTrees.createStatementListWithTwoChildren(); + final Node after = LittleTrees.createStatementListWithThreeChildren( + LittleTrees.createIntegerLiteral(3) + ); + 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.createTreeWithInsertAction(); + 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 replaced node. */ 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 152af3e..d3bd33c 100644 --- a/src/test/java/org/cqfn/astranaut/core/example/LittleTrees.java +++ b/src/test/java/org/cqfn/astranaut/core/example/LittleTrees.java @@ -27,6 +27,7 @@ import java.util.Collections; import org.cqfn.astranaut.core.DifferenceNode; import org.cqfn.astranaut.core.EmptyTree; +import org.cqfn.astranaut.core.Insertion; import org.cqfn.astranaut.core.Node; import org.cqfn.astranaut.core.algorithms.DifferenceTreeBuilder; import org.cqfn.astranaut.core.example.green.ExpressionStatement; @@ -185,6 +186,36 @@ public static Node createStatementListWithThreeChildren(final Node assignable) { ); } + /** + * Creates a tree that has a "insert" action in it. + * @return Root node + */ + public static DifferenceNode createTreeWithInsertAction() { + final Node after = + wrapExpressionWithStatement( + createAssignment( + createVariable("x"), + createIntegerLiteral(1) + ) + ); + final Node inserted = wrapExpressionWithStatement( + createAssignment( + createVariable("y"), + createIntegerLiteral(3) + ) + ); + final DifferenceTreeBuilder builder = new DifferenceTreeBuilder( + createStatementBlock( + after, + createReturnStatement( + createVariable("x") + ) + ) + ); + builder.insertNode(new Insertion(inserted, after)); + return builder.getRoot(); + } + /** * Creates a tree that has a "replace" action in it. * @return Root node