diff --git a/src/main/java/org/cqfn/astranaut/core/Delete.java b/src/main/java/org/cqfn/astranaut/core/Delete.java index d33dc83..622a3bd 100644 --- a/src/main/java/org/cqfn/astranaut/core/Delete.java +++ b/src/main/java/org/cqfn/astranaut/core/Delete.java @@ -168,7 +168,7 @@ public String getProperty(final String name) { @Override public Builder createBuilder() { - return null; + return new Constructor(); } } diff --git a/src/main/java/org/cqfn/astranaut/core/DifferenceNode.java b/src/main/java/org/cqfn/astranaut/core/DifferenceNode.java index 5cd78f0..c8d029c 100644 --- a/src/main/java/org/cqfn/astranaut/core/DifferenceNode.java +++ b/src/main/java/org/cqfn/astranaut/core/DifferenceNode.java @@ -24,7 +24,9 @@ package org.cqfn.astranaut.core; import java.util.ArrayList; +import java.util.LinkedList; import java.util.List; +import java.util.ListIterator; /** * Node containing child nodes, as well as actions on these nodes. @@ -117,6 +119,73 @@ public Node getAfter() { return this.getBranch(DifferenceTreeItem::getAfter); } + /** + * Adds an action that inserts the node after another node. + * If no other node is specified, inserts at the beginning of the children's list. + * @param node Node to be inserted + * @param after Node after which to insert + * @return Result of operation, @return {@code true} if action was added + */ + public boolean insertNodeAfter(final Node node, final Node after) { + boolean result = false; + if (after == null) { + this.children.add(0, new Insert(node)); + result = true; + } else { + final ListIterator iterator = this.children.listIterator(); + while (iterator.hasNext()) { + final Node child = iterator.next(); + if (child instanceof DifferenceNode + && ((DifferenceNode) child).getPrototype() == after) { + iterator.add(new Insert(node)); + result = true; + break; + } + } + } + return result; + } + + /** + * Adds an action that replaces a 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 a 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 @@ -154,7 +223,7 @@ public boolean deleteNode(final Node node) { */ private List initChildrenList() { final int count = this.prototype.getChildCount(); - final List result = new ArrayList<>(count); + final List result = new LinkedList<>(); for (int index = 0; index < count; index = index + 1) { result.add( new DifferenceNode(this, this.prototype.getChild(index)) diff --git a/src/main/java/org/cqfn/astranaut/core/Factory.java b/src/main/java/org/cqfn/astranaut/core/Factory.java index 3c374b5..ccd6d50 100644 --- a/src/main/java/org/cqfn/astranaut/core/Factory.java +++ b/src/main/java/org/cqfn/astranaut/core/Factory.java @@ -55,12 +55,21 @@ public final Builder createBuilder(final String name) { final Type type = this.types.get(name); result = type.createBuilder(); } else { - if (name.equals("Delete")) { - result = new Delete.Constructor(); - } else { - final DraftNode.Constructor draft = new DraftNode.Constructor(); - draft.setName(name); - result = draft; + switch (name) { + case "Insert": + result = new Insert.Constructor(); + break; + case "Replace": + result = new Replace.Constructor(); + break; + case "Delete": + result = new Delete.Constructor(); + break; + default: + final DraftNode.Constructor draft = new DraftNode.Constructor(); + draft.setName(name); + result = draft; + break; } } return result; diff --git a/src/main/java/org/cqfn/astranaut/core/Insert.java b/src/main/java/org/cqfn/astranaut/core/Insert.java new file mode 100644 index 0000000..4aad07a --- /dev/null +++ b/src/main/java/org/cqfn/astranaut/core/Insert.java @@ -0,0 +1,220 @@ +/* + * 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.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * An action that inserts a child element. + * + * @since 1.1.0 + */ +public final class Insert implements Action { + /** + * The type. + */ + public static final Type TYPE = new InsertType(); + + /** + * Child element. + */ + private final Node child; + + /** + * Constructor. + * @param child A child element that will be added. + */ + public Insert(final Node child) { + this.child = child; + } + + @Override + public Node getBefore() { + return null; + } + + @Override + public Node getAfter() { + return this.child; + } + + @Override + public Fragment getFragment() { + return this.child.getFragment(); + } + + @Override + public Type getType() { + return Insert.TYPE; + } + + @Override + public String getData() { + return ""; + } + + @Override + public int getChildCount() { + return 1; + } + + @Override + public Node getChild(final int index) { + final Node node; + if (index == 0) { + node = this.child; + } else { + node = null; + } + return node; + } + + /** + * Type of 'Insert' action. + * + * @since 1.1.0 + */ + private static final class InsertType implements Type { + /** + * The 'Node' string. + */ + private static final String NODE = "Node"; + + /** + * The 'ACTION' string. + */ + private static final String ACTION = "Action"; + + /** + * The 'DELETE' string. + */ + private static final String INSERT = "Insert"; + + /** + * The list of child descriptors. + */ + private static final List CHILDREN = + Collections.singletonList( + new ChildDescriptor( + InsertType.NODE, + false + ) + ); + + /** + * Hierarchy. + */ + private static final List HIERARCHY = + Collections.unmodifiableList( + Arrays.asList( + InsertType.INSERT, + InsertType.ACTION + ) + ); + + /** + * Properties. + */ + private static final Map PROPERTIES = Stream.of( + new String[][] { + {"color", "blue"}, + }).collect(Collectors.toMap(data -> data[0], data -> data[1])); + + @Override + public String getName() { + return InsertType.INSERT; + } + + @Override + public List getChildTypes() { + return InsertType.CHILDREN; + } + + @Override + public List getHierarchy() { + return InsertType.HIERARCHY; + } + + @Override + public String getProperty(final String name) { + return InsertType.PROPERTIES.getOrDefault(name, ""); + } + + @Override + public Builder createBuilder() { + return new Constructor(); + } + } + + /** + * Class for 'Delete' action construction. + * + * @since 1.1.0 + */ + public static final class Constructor implements Builder { + /** + * Child node. + */ + private Node child; + + @Override + public void setFragment(final Fragment fragment) { + // do nothing + } + + @Override + public boolean setData(final String str) { + return str.isEmpty(); + } + + @Override + public boolean setChildrenList(final List list) { + boolean result = false; + if (list.size() == 1) { + this.child = list.get(0); + result = true; + } + return result; + } + + @Override + public boolean isValid() { + return this.child != null; + } + + @Override + public Node createNode() { + Node node = EmptyTree.INSTANCE; + if (this.isValid()) { + node = new Insert(this.child); + } + return node; + } + } +} 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/Replace.java b/src/main/java/org/cqfn/astranaut/core/Replace.java new file mode 100644 index 0000000..7f76983 --- /dev/null +++ b/src/main/java/org/cqfn/astranaut/core/Replace.java @@ -0,0 +1,245 @@ +/* + * 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.Arrays; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * Action that replaces a child element. + * + * @since 1.1.0 + */ +public final class Replace implements Action { + /** + * The type. + */ + public static final Type TYPE = new ReplaceType(); + + /** + * Child element before changes. + */ + private final Node before; + + /** + * Child element after changes. + */ + private final Node after; + + /** + * Constructor. + * @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; + this.after = after; + } + + @Override + public Node getBefore() { + return this.before; + } + + @Override + public Node getAfter() { + return this.after; + } + + @Override + public Fragment getFragment() { + return this.before.getFragment(); + } + + @Override + public Type getType() { + return Replace.TYPE; + } + + @Override + public String getData() { + return ""; + } + + @Override + public int getChildCount() { + return 2; + } + + @Override + public Node getChild(final int index) { + final Node node; + switch (index) { + case 0: + node = this.before; + break; + case 1: + node = this.after; + break; + default: + node = null; + break; + } + return node; + } + + /** + * Type of 'Replace' action. + * + * @since 1.1.0 + */ + private static final class ReplaceType implements Type { + /** + * The 'Node' string. + */ + private static final String NODE = "Node"; + + /** + * The 'ACTION' string. + */ + private static final String ACTION = "Action"; + + /** + * The 'DELETE' string. + */ + private static final String REPLACE = "Replace"; + + /** + * The list of child descriptors. + */ + private static final List CHILDREN = + Arrays.asList( + new ChildDescriptor( + ReplaceType.NODE, + false + ), + new ChildDescriptor( + ReplaceType.NODE, + false + ) + ); + + /** + * Hierarchy. + */ + private static final List HIERARCHY = + Collections.unmodifiableList( + Arrays.asList( + ReplaceType.REPLACE, + ReplaceType.ACTION + ) + ); + + /** + * Properties. + */ + private static final Map PROPERTIES = Stream.of( + new String[][] { + {"color", "blue"}, + }).collect(Collectors.toMap(data -> data[0], data -> data[1])); + + @Override + public String getName() { + return ReplaceType.REPLACE; + } + + @Override + public List getChildTypes() { + return ReplaceType.CHILDREN; + } + + @Override + public List getHierarchy() { + return ReplaceType.HIERARCHY; + } + + @Override + public String getProperty(final String name) { + return ReplaceType.PROPERTIES.getOrDefault(name, ""); + } + + @Override + public Builder createBuilder() { + return new Constructor(); + } + } + + /** + * Class for 'Delete' action construction. + * + * @since 1.1.0 + */ + public static final class Constructor implements Builder { + /** + * Child node before changes. + */ + private Node before; + + /** + * Child node after changes. + */ + private Node after; + + @Override + public void setFragment(final Fragment fragment) { + // do nothing + } + + @Override + public boolean setData(final String str) { + return str.isEmpty(); + } + + @Override + public boolean setChildrenList(final List list) { + boolean result = false; + if (list.size() == 2) { + final Iterator iterator = list.iterator(); + this.before = iterator.next(); + this.after = iterator.next(); + result = true; + } + return result; + } + + @Override + public boolean isValid() { + return this.before != null && this.after != null; + } + + @Override + public Node createNode() { + Node node = EmptyTree.INSTANCE; + if (this.isValid()) { + node = new Replace(this.before, this.after); + } + return node; + } + } +} 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..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,12 @@ 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()); + } for (final Node deleted : mapping.getDeleted()) { result = result & this.deleteNode(deleted); } @@ -80,6 +92,46 @@ public DifferenceNode getRoot() { return this.root; } + /** + * 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 insertion Full information about the node being inserted + * @return Result of operation, {@code true} if action was added + */ + public boolean insertNode(final Insertion insertion) { + boolean result = false; + 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(insertion.getNode(), insertion.getAfter()); + } + return result; + } + + /** + * 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.info.getOrDefault(node, DifferenceTreeBuilder.DEFAULT_INFO).getParent(); + 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 @@ -87,7 +139,8 @@ public DifferenceNode getRoot() { */ 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); } @@ -99,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/BottomUpMappingAlgorithm.java b/src/main/java/org/cqfn/astranaut/core/algorithms/mapping/BottomUpAlgorithm.java similarity index 55% rename from src/main/java/org/cqfn/astranaut/core/algorithms/mapping/BottomUpMappingAlgorithm.java rename to src/main/java/org/cqfn/astranaut/core/algorithms/mapping/BottomUpAlgorithm.java index 3e70db3..bace428 100644 --- a/src/main/java/org/cqfn/astranaut/core/algorithms/mapping/BottomUpMappingAlgorithm.java +++ b/src/main/java/org/cqfn/astranaut/core/algorithms/mapping/BottomUpAlgorithm.java @@ -28,10 +28,12 @@ import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; +import java.util.LinkedList; import java.util.List; 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; @@ -44,7 +46,8 @@ * * @since 1.1.0 */ -class BottomUpMappingAlgorithm { +@SuppressWarnings("PMD.TooManyMethods") +class BottomUpAlgorithm { /** * Set of node hashes. */ @@ -61,14 +64,19 @@ class BottomUpMappingAlgorithm { private final Map parents; /** - * Not yet processed nodes from the 'left' tree. + * A set of nodes that have not yet been processed. */ - private final Set left; + private final Set unprocessed; /** - * Not yet processed nodes from the 'right' tree. + * Sorted nodes from the 'left' tree. */ - private final Set right; + private final List left; + + /** + * Sorted nodes from the 'right' tree. + */ + private final List right; /** * Left-to-right mapping. @@ -80,6 +88,16 @@ class BottomUpMappingAlgorithm { */ private final Map rtl; + /** + * Set containing inserted nodes. + */ + private final Set inserted; + + /** + * Map containing replaces nodes. + */ + private final Map replaced; + /** * Set of deleted nodes. */ @@ -90,14 +108,17 @@ class BottomUpMappingAlgorithm { * @param left Root node of the 'left' tree * @param right Root node of the 'right' tree */ - BottomUpMappingAlgorithm(final Node left, final Node right) { + BottomUpAlgorithm(final Node left, final Node right) { this.hashes = new AbsoluteHash(); this.depth = new Depth(); this.parents = new HashMap<>(); - this.left = this.createNodeSet(left); - this.right = this.createNodeSet(right); + this.unprocessed = new HashSet<>(); + this.left = this.createNodeList(left); + 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<>(); } @@ -121,45 +142,32 @@ void execute() { * @return Result of mapping */ Mapping getResult() { - return new Mapping() { - @Override - public Node getRight(final Node node) { - return BottomUpMappingAlgorithm.this.ltr.get(node); - } - - @Override - public Node getLeft(final Node node) { - return BottomUpMappingAlgorithm.this.rtl.get(node); - } - - @Override - public Set getDeleted() { - return Collections.unmodifiableSet(BottomUpMappingAlgorithm.this.deleted); - } - }; + return new Result(this); } /** - * Creates an initial set of nodes suitable for processing from the tree. + * Creates an initial list of nodes suitable for processing from the tree. * @param root The root of the tree - * @return Set of nodes + * @return List of nodes where leaves are placed first. */ - private Set createNodeSet(final Node root) { - final Set set = new HashSet<>(); - this.createNodeSet(root, null, set); - return set; + private List createNodeList(final Node root) { + final List list = new LinkedList<>(); + this.createNodeList(root, null, list); + return list; } /** * 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 + * @param list The resulting list */ - private void createNodeSet(final Node node, final Node parent, final Set set) { - set.add(node); + private void createNodeList(final Node node, final Node parent, final List list) { this.parents.put(node, parent); - node.forEachChild(child -> this.createNodeSet(child, node, set)); + node.forEachChild(child -> this.createNodeList(child, node, list)); + list.add(node); + final boolean added = this.unprocessed.add(node); + assert added; } /** @@ -201,9 +209,9 @@ private Map> performInitialMapping() { 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) + (first, second) -> Integer.compare( + this.depth.calculate(second), + this.depth.calculate(first) ) ); for (final Node node : sorted) { @@ -230,8 +238,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.unprocessed.remove(node); + this.unprocessed.remove(related); this.ltr.put(node, related); this.rtl.put(related, node); final int count = node.getChildCount(); @@ -252,12 +260,14 @@ private Node findPartiallyMappedLeftNode() { 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; + if (this.unprocessed.contains(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)) { + result = node; + break; + } } } } @@ -289,19 +299,133 @@ private Node mapPartiallyMappedLeftNode(final Node node) { || !node.getData().equals(related.getData())) { break; } - this.right.remove(related); + this.unprocessed.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); + this.unprocessed.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.mapChildrenIfInserted(before, after); + } else if (sign > 0) { + this.mapChildrenIfDeleted(before); + } 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 + * 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.unprocessed.remove(first); + this.unprocessed.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.unprocessed.remove(child); + } + } + } + + /** + * Mapping result. + * + * @since 1.1.0 + */ + private static final class Result implements Mapping { + /** + * Structure from which the mapping results can be taken. + */ + private final BottomUpAlgorithm data; + + /** + * Constructor. + * @param data Structure from which the mapping results can be taken + */ + private Result(final BottomUpAlgorithm data) { + this.data = data; + } + + @Override + public Node getRight(final Node node) { + return this.data.ltr.get(node); + } + + @Override + 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); + } + + @Override + public Set getDeleted() { + return Collections.unmodifiableSet(this.data.deleted); + } + } } 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 index 4eba07a..806c4d1 100644 --- a/src/main/java/org/cqfn/astranaut/core/algorithms/mapping/BottomUpMapper.java +++ b/src/main/java/org/cqfn/astranaut/core/algorithms/mapping/BottomUpMapper.java @@ -35,7 +35,7 @@ public final class BottomUpMapper implements Mapper { @Override public Mapping map(final Node left, final Node right) { - final BottomUpMappingAlgorithm algorithm = new BottomUpMappingAlgorithm(left, right); + final BottomUpAlgorithm algorithm = new BottomUpAlgorithm(left, right); algorithm.execute(); return algorithm.getResult(); } 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..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 @@ -23,7 +23,9 @@ */ package org.cqfn.astranaut.core.algorithms.mapping; +import java.util.Map; import java.util.Set; +import org.cqfn.astranaut.core.Insertion; import org.cqfn.astranaut.core.Node; /** @@ -49,6 +51,20 @@ 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. + * @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/main/java/org/cqfn/astranaut/core/utils/deserializer/ActionList.java b/src/main/java/org/cqfn/astranaut/core/utils/deserializer/ActionList.java index e3a31f5..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 @@ -23,9 +23,12 @@ */ package org.cqfn.astranaut.core.utils.deserializer; +import java.util.HashMap; import java.util.HashSet; +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; @@ -35,6 +38,16 @@ * @since 1.1.0 */ public class ActionList { + /** + * Collection of nodes to be inserted. + */ + private final Set insert; + + /** + * Collection of nodes to be replaced (node before changes -> node after changes). + */ + private final Map replace; + /** * Set of nodes to be deleted. */ @@ -44,6 +57,8 @@ public class ActionList { * Constructor. */ public ActionList() { + this.insert = new HashSet<>(); + this.replace = new HashMap<>(); this.delete = new HashSet<>(); } @@ -52,7 +67,26 @@ public ActionList() { * @return Checking result */ public boolean hasActions() { - return !this.delete.isEmpty(); + return !this.insert.isEmpty() || !this.replace.isEmpty() || !this.delete.isEmpty(); + } + + /** + * 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 into, final Node after) { + this.insert.add(new Insertion(node, into, after)); + } + + /** + * Adds the node to the list of nodes to be replaced. + * @param node Node to be replaced + * @param replacement Node to be replaced by + */ + public void replaceNode(final Node node, final Node replacement) { + this.replace.put(node, replacement); } /** @@ -70,6 +104,12 @@ public void deleteNode(final Node node) { */ public DifferenceNode convertTreeToDifferenceTree(final Node root) { final DifferenceTreeBuilder builder = new DifferenceTreeBuilder(root); + for (final Insertion insertion : this.insert) { + builder.insertNode(insertion); + } + for (final Map.Entry pair : this.replace.entrySet()) { + builder.replaceNode(pair.getKey(), pair.getValue()); + } for (final Node node : this.delete) { builder.deleteNode(node); } 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 6420f59..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 @@ -29,7 +29,9 @@ import org.cqfn.astranaut.core.Delete; import org.cqfn.astranaut.core.EmptyTree; import org.cqfn.astranaut.core.Factory; +import org.cqfn.astranaut.core.Insert; import org.cqfn.astranaut.core.Node; +import org.cqfn.astranaut.core.Replace; /** * Node descriptor represented as it is stored in the JSON file. @@ -95,10 +97,23 @@ private List convertChildren(final Factory factory, final ActionList actio final List list = new ArrayList<>(this.children.size()); for (final NodeDescriptor child : this.children) { final Node converted = child.convert(factory, actions); - if (converted instanceof Delete) { - final Node before = ((Delete) converted).getBefore(); - list.add(before); - actions.deleteNode(before); + if (converted instanceof Insert) { + final Node node = ((Insert) converted).getAfter(); + Node after = null; + final int size = list.size(); + if (size > 0) { + after = list.get(size - 1); + } + actions.insertNodeAfter(node, null, after); + } else if (converted instanceof Replace) { + final Replace action = (Replace) converted; + final Node node = action.getBefore(); + list.add(node); + actions.replaceNode(node, action.getAfter()); + } else if (converted instanceof Delete) { + final Node node = ((Delete) converted).getBefore(); + list.add(node); + actions.deleteNode(node); } else { list.add(converted); } diff --git a/src/test/java/org/cqfn/astranaut/core/ActionTest.java b/src/test/java/org/cqfn/astranaut/core/ActionTest.java new file mode 100644 index 0000000..257bc70 --- /dev/null +++ b/src/test/java/org/cqfn/astranaut/core/ActionTest.java @@ -0,0 +1,173 @@ +/* + * 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.Arrays; +import java.util.Collections; +import java.util.List; +import org.cqfn.astranaut.core.example.LittleTrees; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +/** + * Tests covering actions, i.e. {@link Action} interface and inherited classes. + * + * @since 1.1.0 + */ +class ActionTest { + /** + * The 'Insert' type. + */ + private static final String INSERT_TYPE = "Insert"; + + /** + * The 'Replace' type. + */ + private static final String REPLACE_TYPE = "Replace"; + + /** + * The 'Delete' type. + */ + private static final String DELETE_TYPE = "Delete"; + + /** + * The 'color' property. + */ + private static final String COLOR_PROPERTY = "color"; + + /** + * The expected color property. + */ + private static final String EXPECTED_COLOR = "blue"; + + /** + * Testing {@link Insert} action. + */ + @Test + void testInsertAction() { + final Node inserted = LittleTrees.createReturnStatement(null); + final Action action = new Insert(inserted); + Assertions.assertEquals(EmptyFragment.INSTANCE, action.getFragment()); + Assertions.assertEquals("", action.getData()); + Assertions.assertEquals(1, action.getChildCount()); + Assertions.assertEquals(inserted, action.getChild(0)); + Assertions.assertNull(action.getChild(1)); + final Type type = action.getType(); + Assertions.assertEquals(ActionTest.INSERT_TYPE, type.getName()); + final List descriptors = type.getChildTypes(); + Assertions.assertFalse(descriptors.isEmpty()); + final List hierarchy = type.getHierarchy(); + Assertions.assertFalse(hierarchy.isEmpty()); + Assertions.assertEquals(type.getName(), hierarchy.get(0)); + Assertions.assertEquals( + ActionTest.EXPECTED_COLOR, + type.getProperty(ActionTest.COLOR_PROPERTY) + ); + final Builder builder = type.createBuilder(); + builder.setFragment(EmptyFragment.INSTANCE); + Assertions.assertTrue(builder.setData("")); + Assertions.assertFalse(builder.setData("abracadabra")); + Assertions.assertFalse(builder.isValid()); + Node created = builder.createNode(); + Assertions.assertEquals(EmptyTree.INSTANCE, created); + Assertions.assertTrue(builder.setChildrenList(Collections.singletonList(inserted))); + Assertions.assertFalse(builder.setChildrenList(Arrays.asList(inserted, inserted))); + created = builder.createNode(); + Assertions.assertEquals(ActionTest.INSERT_TYPE, created.getTypeName()); + } + + /** + * Testing {@link Replace} action. + */ + @Test + void testReplaceAction() { + final Node before = LittleTrees.createVariable("x"); + final Node after = LittleTrees.createIntegerLiteral(0); + final Action action = new Replace(before, after); + Assertions.assertEquals(EmptyFragment.INSTANCE, action.getFragment()); + Assertions.assertEquals("", action.getData()); + Assertions.assertEquals(2, action.getChildCount()); + Assertions.assertEquals(before, action.getChild(0)); + Assertions.assertEquals(after, action.getChild(1)); + Assertions.assertNull(action.getChild(2)); + final Type type = action.getType(); + Assertions.assertEquals(ActionTest.REPLACE_TYPE, type.getName()); + final List descriptors = type.getChildTypes(); + Assertions.assertFalse(descriptors.isEmpty()); + final List hierarchy = type.getHierarchy(); + Assertions.assertFalse(hierarchy.isEmpty()); + Assertions.assertEquals(type.getName(), hierarchy.get(0)); + Assertions.assertEquals( + ActionTest.EXPECTED_COLOR, + type.getProperty(ActionTest.COLOR_PROPERTY) + ); + final Builder builder = type.createBuilder(); + builder.setFragment(EmptyFragment.INSTANCE); + Assertions.assertTrue(builder.setData("")); + Assertions.assertFalse(builder.setData("it's a kind of magic")); + Assertions.assertFalse(builder.isValid()); + Node created = builder.createNode(); + Assertions.assertEquals(EmptyTree.INSTANCE, created); + Assertions.assertTrue(builder.setChildrenList(Arrays.asList(before, after))); + Assertions.assertFalse(builder.setChildrenList(Collections.singletonList(before))); + created = builder.createNode(); + Assertions.assertEquals(ActionTest.REPLACE_TYPE, created.getTypeName()); + } + + /** + * Testing {@link Delete} action. + */ + @Test + void testDeleteAction() { + final Node deleted = LittleTrees.createReturnStatement(null); + final Action action = new Delete(deleted); + Assertions.assertEquals(EmptyFragment.INSTANCE, action.getFragment()); + Assertions.assertEquals("", action.getData()); + Assertions.assertEquals(1, action.getChildCount()); + Assertions.assertEquals(deleted, action.getChild(0)); + Assertions.assertNull(action.getChild(1)); + final Type type = action.getType(); + Assertions.assertEquals(ActionTest.DELETE_TYPE, type.getName()); + final List descriptors = type.getChildTypes(); + Assertions.assertFalse(descriptors.isEmpty()); + final List hierarchy = type.getHierarchy(); + Assertions.assertFalse(hierarchy.isEmpty()); + Assertions.assertEquals(type.getName(), hierarchy.get(0)); + Assertions.assertEquals( + ActionTest.EXPECTED_COLOR, + type.getProperty(ActionTest.COLOR_PROPERTY) + ); + final Builder builder = type.createBuilder(); + builder.setFragment(EmptyFragment.INSTANCE); + Assertions.assertTrue(builder.setData("")); + Assertions.assertFalse(builder.setData("I hate syntax trees")); + Assertions.assertFalse(builder.isValid()); + Node created = builder.createNode(); + Assertions.assertEquals(EmptyTree.INSTANCE, created); + Assertions.assertTrue(builder.setChildrenList(Collections.singletonList(deleted))); + Assertions.assertFalse(builder.setChildrenList(Arrays.asList(deleted, deleted))); + created = builder.createNode(); + Assertions.assertEquals(ActionTest.DELETE_TYPE, created.getTypeName()); + } +} diff --git a/src/test/java/org/cqfn/astranaut/core/DifferenceNodeTest.java b/src/test/java/org/cqfn/astranaut/core/DifferenceNodeTest.java index 986263f..25694fe 100644 --- a/src/test/java/org/cqfn/astranaut/core/DifferenceNodeTest.java +++ b/src/test/java/org/cqfn/astranaut/core/DifferenceNodeTest.java @@ -23,6 +23,7 @@ */ package org.cqfn.astranaut.core; +import org.cqfn.astranaut.core.example.LittleTrees; import org.cqfn.astranaut.core.example.green.GreenFactory; import org.cqfn.astranaut.core.exceptions.BaseException; import org.cqfn.astranaut.core.utils.FilesReader; @@ -41,13 +42,91 @@ class DifferenceNodeTest { */ private static final String TESTS_PATH = "src/test/resources/json/"; + /** + * File name with tree before 'Delete' action. + */ + private static final String TREE_BEFO_DELETE = "before_delete_action.json"; + + /** + * File name with tree after 'Delete' action. + */ + private static final String TREE_AFTER_DELETE = "after_delete_action.json"; + + /** + * File name with tree before 'Replace' action. + */ + private static final String TREE_BEFORE_REPL = "before_replace_action.json"; + + /** + * File name with tree after 'Replace' action. + */ + private static final String TREE_AFTER_REPL = "after_replace_action.json"; + + /** + * File name with tree containing 'Insert' action. + */ + private static final String TREE_WITH_INSERT = "tree_containing_insert_action.json"; + + /** + * File name with tree containing 'Replace' action. + */ + private static final String TREE_WITH_REPLACE = "tree_containing_replace_action.json"; + /** * File name with tree containing 'Delete' action. */ private static final String TREE_WITH_DELETE = "tree_containing_delete_action.json"; /** - * Testing {@link DifferenceNode#getBefore()} method. + * Testing {@link DifferenceNode#getBefore()} method with inserted node. + */ + @Test + void testInsertGetBefore() { + final Node root = this.loadTree(DifferenceNodeTest.TREE_WITH_INSERT); + Assertions.assertTrue(root instanceof DifferenceNode); + final DifferenceNode diff = (DifferenceNode) root; + final Node actual = diff.getBefore(); + Assertions.assertNotEquals(EmptyTree.INSTANCE, actual); + final Node expected = this.loadTree(DifferenceNodeTest.TREE_AFTER_DELETE); + Assertions.assertTrue(expected.deepCompare(actual)); + } + + /** + * Testing {@link DifferenceNode#getAfter()} method with inserted node. + */ + @Test + void testInsertGetAfter() { + final Node root = this.loadTree(DifferenceNodeTest.TREE_WITH_INSERT); + Assertions.assertTrue(root instanceof DifferenceNode); + final DifferenceNode diff = (DifferenceNode) root; + final Node actual = diff.getAfter(); + Assertions.assertNotEquals(EmptyTree.INSTANCE, actual); + final Node expected = this.loadTree(DifferenceNodeTest.TREE_BEFO_DELETE); + Assertions.assertTrue(expected.deepCompare(actual)); + } + + /** + * Testing tree loading / composing with replaced node. + */ + @Test + void testReplace() { + final Node root = this.loadTree(DifferenceNodeTest.TREE_WITH_REPLACE); + Assertions.assertTrue(root instanceof DifferenceNode); + final DifferenceNode diff = (DifferenceNode) root; + final Node before = diff.getBefore(); + Assertions.assertNotEquals(EmptyTree.INSTANCE, before); + Assertions.assertTrue( + before.deepCompare(this.loadTree(DifferenceNodeTest.TREE_BEFORE_REPL)) + ); + final Node after = diff.getAfter(); + Assertions.assertNotEquals(EmptyTree.INSTANCE, after); + Assertions.assertTrue( + after.deepCompare(this.loadTree(DifferenceNodeTest.TREE_AFTER_REPL)) + ); + } + + /** + * Testing {@link DifferenceNode#getBefore()} method with deleted node. */ @Test void testDeleteGetBefore() { @@ -56,12 +135,12 @@ void testDeleteGetBefore() { final DifferenceNode diff = (DifferenceNode) root; final Node actual = diff.getBefore(); Assertions.assertNotEquals(EmptyTree.INSTANCE, actual); - final Node expected = this.loadTree("before_delete_action.json"); + final Node expected = this.loadTree(DifferenceNodeTest.TREE_BEFO_DELETE); Assertions.assertTrue(expected.deepCompare(actual)); } /** - * Testing {@link DifferenceNode#getAfter()} method. + * Testing {@link DifferenceNode#getAfter()} method with deleted node. */ @Test void testDeleteGetAfter() { @@ -70,10 +149,48 @@ void testDeleteGetAfter() { final DifferenceNode diff = (DifferenceNode) root; final Node actual = diff.getAfter(); Assertions.assertNotEquals(EmptyTree.INSTANCE, actual); - final Node expected = this.loadTree("after_delete_action.json"); + final Node expected = this.loadTree(DifferenceNodeTest.TREE_AFTER_DELETE); Assertions.assertTrue(expected.deepCompare(actual)); } + /** + * Tests the case where a node is inserted at the start position of the child list. + */ + @Test + void testInsertNodeFirst() { + final Node first = LittleTrees.createReturnStatement(null); + final Node second = LittleTrees.wrapExpressionWithStatement( + LittleTrees.createAssignment( + LittleTrees.createVariable("x"), + LittleTrees.createIntegerLiteral(0) + ) + ); + final Node before = LittleTrees.createStatementBlock(first); + final Node after = LittleTrees.createStatementBlock(second, first); + final DifferenceNode diff = new DifferenceNode(before); + final boolean result = diff.insertNodeAfter(second, null); + Assertions.assertTrue(result); + Assertions.assertTrue(before.deepCompare(diff.getBefore())); + Assertions.assertTrue(after.deepCompare(diff.getAfter())); + } + + /** + * Tests the case where an attempt to insert a node fails. + */ + @Test + void testInsertNodeFails() { + final DifferenceNode diff = new DifferenceNode( + LittleTrees.createStatementBlock( + LittleTrees.createReturnStatement(null) + ) + ); + final boolean result = diff.insertNodeAfter( + LittleTrees.createVariable("x"), + LittleTrees.createVariable("y") + ); + Assertions.assertFalse(result); + } + /** * Returns content of the specified file. * @param name The name of the file 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 b5460c9..124d8dd 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,58 @@ * @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. + */ + @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()); @@ -52,4 +100,29 @@ void testTreeWithDeletedNode() { Assertions.assertTrue(before.deepCompare(diff.getBefore())); Assertions.assertTrue(after.deepCompare(diff.getAfter())); } + + /** + * Testing the construction of a difference tree with a deleted node. + * This node is just below the root, so that the number and type of children of the root + * do not change. + */ + @Test + void testTreeWithDeletedNodeInDepth() { + final Node before = LittleTrees.createStatementBlock( + LittleTrees.createStatementListWithThreeChildren( + LittleTrees.createIntegerLiteral(2) + ) + ); + final Node after = LittleTrees.createStatementBlock( + 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.createTreeWithDeleteActionInDepth(); + Assertions.assertTrue(expected.deepCompare(diff)); + Assertions.assertTrue(before.deepCompare(diff.getBefore())); + Assertions.assertTrue(after.deepCompare(diff.getAfter())); + } } 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 07aeb08..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; @@ -41,7 +42,7 @@ * * @since 1.1.0 */ -@SuppressWarnings("PMD.ProhibitPublicStaticMethods") +@SuppressWarnings({"PMD.ProhibitPublicStaticMethods", "PMD.TooManyMethods"}) public final class LittleTrees { /** * Private constructor. @@ -162,9 +163,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 +177,7 @@ public static Node createStatementListWithThreeChildren() { wrapExpressionWithStatement( createAssignment( createVariable("y"), - createIntegerLiteral(2) + assignable ) ), createReturnStatement( @@ -184,6 +186,66 @@ public static Node createStatementListWithThreeChildren() { ); } + /** + * 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 + */ + 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 @@ -203,7 +265,7 @@ public static DifferenceNode createTreeWithDeleteAction() { createIntegerLiteral(1) ) ), - victim, + victim, createReturnStatement( createVariable("x") ) @@ -212,4 +274,37 @@ public static DifferenceNode createTreeWithDeleteAction() { builder.deleteNode(victim); return builder.getRoot(); } + + /** + * Creates a tree that has a "delete" action in it. + * This action is just below the root, so that the number and type of children of the root + * do not change. + * @return Root node + */ + public static DifferenceNode createTreeWithDeleteActionInDepth() { + final Node victim = wrapExpressionWithStatement( + createAssignment( + createVariable("y"), + createIntegerLiteral(2) + ) + ); + final DifferenceTreeBuilder builder = new DifferenceTreeBuilder( + createStatementBlock( + createStatementBlock( + wrapExpressionWithStatement( + createAssignment( + createVariable("x"), + createIntegerLiteral(1) + ) + ), + victim, + createReturnStatement( + createVariable("x") + ) + ) + ) + ); + builder.deleteNode(victim); + return builder.getRoot(); + } } diff --git a/src/test/resources/json/after_replace_action.json b/src/test/resources/json/after_replace_action.json new file mode 100644 index 0000000..c881cc2 --- /dev/null +++ b/src/test/resources/json/after_replace_action.json @@ -0,0 +1,52 @@ +{ + "root": { + "type": "StatementBlock", + "children": [ + { + "type": "ExpressionStatement", + "children": [ + { + "type": "SimpleAssignment", + "children": [ + { + "type": "Variable", + "data": "x" + }, + { + "type": "IntegerLiteral", + "data": "1" + } + ] + } + ] + }, + { + "type": "ExpressionStatement", + "children": [ + { + "type": "SimpleAssignment", + "children": [ + { + "type": "Variable", + "data": "y" + }, + { + "type": "IntegerLiteral", + "data": "33" + } + ] + } + ] + }, + { + "type": "Return", + "children": [ + { + "type": "Variable", + "data": "x" + } + ] + } + ] + } +} \ No newline at end of file diff --git a/src/test/resources/json/before_replace_action.json b/src/test/resources/json/before_replace_action.json new file mode 100644 index 0000000..7861802 --- /dev/null +++ b/src/test/resources/json/before_replace_action.json @@ -0,0 +1,52 @@ +{ + "root": { + "type": "StatementBlock", + "children": [ + { + "type": "ExpressionStatement", + "children": [ + { + "type": "SimpleAssignment", + "children": [ + { + "type": "Variable", + "data": "x" + }, + { + "type": "IntegerLiteral", + "data": "1" + } + ] + } + ] + }, + { + "type": "ExpressionStatement", + "children": [ + { + "type": "SimpleAssignment", + "children": [ + { + "type": "Variable", + "data": "y" + }, + { + "type": "Variable", + "data": "x" + } + ] + } + ] + }, + { + "type": "Return", + "children": [ + { + "type": "Variable", + "data": "x" + } + ] + } + ] + } +} \ No newline at end of file diff --git a/src/test/resources/json/tree_containing_insert_action.json b/src/test/resources/json/tree_containing_insert_action.json new file mode 100644 index 0000000..d8c085f --- /dev/null +++ b/src/test/resources/json/tree_containing_insert_action.json @@ -0,0 +1,57 @@ +{ + "root": { + "type": "StatementBlock", + "children": [ + { + "type": "ExpressionStatement", + "children": [ + { + "type": "SimpleAssignment", + "children": [ + { + "type": "Variable", + "data": "x" + }, + { + "type": "IntegerLiteral", + "data": "1" + } + ] + } + ] + }, + { + "type": "Insert", + "children": [ + { + "type": "ExpressionStatement", + "children": [ + { + "type": "SimpleAssignment", + "children": [ + { + "type": "Variable", + "data": "y" + }, + { + "type": "IntegerLiteral", + "data": "2" + } + ] + } + ] + } + ] + }, + { + "type": "Return", + "children": [ + { + "type": "Variable", + "data": "x" + } + ] + } + ] + } +} \ No newline at end of file diff --git a/src/test/resources/json/tree_containing_replace_action.json b/src/test/resources/json/tree_containing_replace_action.json new file mode 100644 index 0000000..2d1053e --- /dev/null +++ b/src/test/resources/json/tree_containing_replace_action.json @@ -0,0 +1,61 @@ +{ + "root": { + "type": "StatementBlock", + "children": [ + { + "type": "ExpressionStatement", + "children": [ + { + "type": "SimpleAssignment", + "children": [ + { + "type": "Variable", + "data": "x" + }, + { + "type": "IntegerLiteral", + "data": "1" + } + ] + } + ] + }, + { + "type": "ExpressionStatement", + "children": [ + { + "type": "SimpleAssignment", + "children": [ + { + "type": "Variable", + "data": "y" + }, + { + "type": "Replace", + "children": [ + { + "type": "Variable", + "data": "x" + }, + { + "type": "IntegerLiteral", + "data": "33" + } + ] + } + ] + } + ] + }, + { + "type": "Return", + "children": [ + { + "type": "Variable", + "data": "x" + } + ] + } + ] + } +} \ No newline at end of file