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 deleted file mode 100644 index daa7f51..0000000 --- a/src/main/java/org/cqfn/astranaut/core/algorithms/mapping/BottomUpAlgorithm.java +++ /dev/null @@ -1,468 +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.ArrayList; -import java.util.Collections; -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; -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 - */ -@SuppressWarnings("PMD.TooManyMethods") -class BottomUpAlgorithm { - /** - * Set of node hashes. - */ - private final Hash hashes; - - /** - * Set of node depths. - */ - private final Depth depth; - - /** - * Relationship of the nodes to their parents. - */ - private final Map parents; - - /** - * A set of nodes that have not yet been processed. - */ - private final Set unprocessed; - - /** - * Sorted nodes from the 'left' tree. - */ - private final List left; - - /** - * Sorted nodes from the 'right' tree. - */ - private final List right; - - /** - * Left-to-right mapping. - */ - private final Map ltr; - - /** - * Right-to-left mapping. - */ - private final Map rtl; - - /** - * Set containing inserted nodes. - */ - private final Set inserted; - - /** - * Map containing replaces nodes. - */ - private final Map replaced; - - /** - * Set of deleted nodes. - */ - private final Set deleted; - - /** - * Constructor. - * @param left Root node of the 'left' tree - * @param right Root node of the 'right' tree - */ - BottomUpAlgorithm(final Node left, final Node right) { - this.hashes = new AbsoluteHash(); - this.depth = new Depth(); - this.parents = new HashMap<>(); - 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<>(); - } - - /** - * Performs the mapping. - */ - void execute() { - final DraftMapping draft = this.performInitialMapping(); - this.absorbLargestSubtrees(draft); - Node node = this.findPartiallyMappedLeftNode(); - while (node != null) { - node = this.mapPartiallyMappedLeftNode(node); - if (node == null) { - node = this.findPartiallyMappedLeftNode(); - } - } - } - - /** - * Returns result of mapping. - * @return Result of mapping - */ - Mapping getResult() { - return new Result(this); - } - - /** - * Creates an initial list of nodes suitable for processing from the tree. - * @param root The root of the tree - * @return List of nodes where leaves are placed first. - */ - 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 list The resulting list - */ - private void createNodeList(final Node node, final Node parent, final List list) { - this.parents.put(node, parent); - node.forEachChild(child -> this.createNodeList(child, node, list)); - list.add(node); - final boolean added = this.unprocessed.add(node); - assert added; - } - - /** - * Performs hash calculation of nodes from the 'right' set. - * @return The hash relation to the set 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 Set set = - result.computeIfAbsent(hash, k -> new HashSet<>()); - set.add(node); - } - return result; - } - - /** - * Performs initial (draft) node mapping. - * @return Relationships of nodes to lists of nodes to which they can be mapped to - */ - private DraftMapping performInitialMapping() { - final DraftMapping result = new DraftMapping(); - final Map> relation = this.calculateRightHashes(); - for (final Node node : this.left) { - final int hash = this.hashes.calculate(node); - final Set set = relation.get(hash); - if (set != null) { - result.addRelation(node, set); - } - } - return result; - } - - /** - * Selects the largest size subtrees from the initial node relation and maps them. - * @param draft Initial node relation - */ - private void absorbLargestSubtrees(final DraftMapping draft) { - final List sorted = new ArrayList<>(draft.getLeftNodes()); - sorted.sort( - (first, second) -> Integer.compare( - this.depth.calculate(second), - this.depth.calculate(first) - ) - ); - for (final Node node : sorted) { - final Set related = draft.getRelation(node); - if (related != null && related.size() == 1 && !this.ltr.containsKey(node)) { - this.mapSubtreesWithTheSameHash(node, related.iterator().next(), draft); - } - if (draft.isEmpty()) { - break; - } - } - } - - /** - * 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 DraftMapping draft) { - assert this.hashes.calculate(node) == this.hashes.calculate(related); - draft.removeRelation(node, related); - this.unprocessed.remove(node); - 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 first = node.getChild(index); - final Node second = related.getChild(index); - 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(); - 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; - } - } - } - } - 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.unprocessed.remove(related); - this.ltr.put(node, related); - this.rtl.put(related, node); - final boolean mapped = this.mapChildren(node, related); - assert mapped; - next = this.parents.get(node); - } while (false); - this.unprocessed.remove(node); - return next; - } - - /** - * Maps the child nodes of partially mapped nodes. - * @param before Node before changes - * @param after Node after changes - * @return Mapping result, {@code true} if at least one action has been added - */ - private boolean mapChildren(final Node before, final Node after) { - final int sign = Integer.compare(before.getChildCount(), after.getChildCount()); - final boolean result; - if (sign < 0) { - result = this.mapChildrenIfInserted(before, after); - } else if (sign > 0) { - result = this.mapChildrenIfDeleted(before); - } else { - this.mapChildrenIfReplaced(before, after); - result = true; - } - return result; - } - - /** - * 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 - * @return Mapping result, {@code true} if at least one action has been added - */ - private boolean mapChildrenIfInserted(final Node before, final Node after) { - final int count = after.getChildCount(); - boolean result = false; - 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); - result = true; - } - } - return result; - } - - /** - * 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); - if (!this.mapTwoNodes(first, second)) { - 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 - * @return Mapping result, {@code true} if at least one action has been added - */ - private boolean mapChildrenIfDeleted(final Node before) { - final int count = before.getChildCount(); - boolean result = false; - 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); - result = true; - } - } - return result; - } - - /** - * Trying to map the two nodes. - * @param before Node before changes - * @param after Node after changes - * @return Mapping result, {@code true} if nodes have been mapped - */ - private boolean mapTwoNodes(final Node before, final Node after) { - assert !this.ltr.containsKey(before); - boolean result = false; - if (before.getTypeName().equals(after.getTypeName()) - && before.getData().equals(after.getData())) { - this.unprocessed.remove(before); - this.unprocessed.remove(after); - this.ltr.put(before, after); - this.rtl.put(after, before); - final boolean mapped = this.mapChildren(before, after); - assert mapped; - result = true; - } - return result; - } - - /** - * 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 deleted file mode 100644 index 806c4d1..0000000 --- a/src/main/java/org/cqfn/astranaut/core/algorithms/mapping/BottomUpMapper.java +++ /dev/null @@ -1,42 +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 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 BottomUpAlgorithm algorithm = new BottomUpAlgorithm(left, right); - algorithm.execute(); - return algorithm.getResult(); - } -} diff --git a/src/main/java/org/cqfn/astranaut/core/algorithms/mapping/DraftMapping.java b/src/main/java/org/cqfn/astranaut/core/algorithms/mapping/DraftMapping.java deleted file mode 100644 index 8ace7f1..0000000 --- a/src/main/java/org/cqfn/astranaut/core/algorithms/mapping/DraftMapping.java +++ /dev/null @@ -1,117 +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.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; -import org.cqfn.astranaut.core.Node; - -/** - * A mapping that has ambiguities, that is, one-to-many relations, - * that is, that offers multiple variants of 'right' tree nodes that are somehow related - * to a single 'left' tree node. - * - * @since 1.1.3 - */ -final class DraftMapping { - /** - * Left-to-right relations. - */ - private final Map> ltr; - - /** - * Right-to-left relations. - */ - private final Map> rtl; - - /** - * Constructor. - */ - DraftMapping() { - this.ltr = new HashMap<>(); - this.rtl = new HashMap<>(); - } - - /** - * Adds the relation of a node from the 'left' tree to some set of nodes from the 'right' tree. - * @param left Node from the 'left' tree - * @param right Related nodes from the 'right' tree - */ - public void addRelation(final Node left, final Set right) { - this.ltr.put(left, right); - for (final Node node : right) { - final Set set = this.rtl.computeIfAbsent(node, k -> new HashSet<>()); - set.add(left); - } - } - - /** - * Returns the relation of a node from the 'left' tree to some set of nodes. - * from the 'right' tree - * @param left Node from the 'left' tree - * @return Related nodes from the 'right' tree or empty set - */ - public Set getRelation(final Node left) { - return this.ltr.getOrDefault(left, Collections.emptySet()); - } - - /** - * Removes the relationship of nodes from the 'left' and 'right' trees. - * This reduces ambiguity, i.e. if a 'left' node has been correlated with a 'right' node, - * it can no longer be correlated with any other 'right' node. - * There should be no ambiguities remaining at the end of the mapping. - * @param left Node from the 'left' tree - * @param right Node from the 'right' tree - */ - public void removeRelation(final Node left, final Node right) { - this.ltr.remove(left); - if (this.rtl.containsKey(right)) { - for (final Node node : this.rtl.get(right)) { - final Set set = this.ltr.get(node); - if (set != null) { - set.remove(right); - } - } - } - } - - /** - * Returns set of mapped nodes from the 'left' tree. - * @return Set of nodes that have relations - */ - public Set getLeftNodes() { - return this.ltr.keySet(); - } - - /** - * Returns {@code} if this collection contains no mappings. - * @return Checking result - */ - public boolean isEmpty() { - return this.ltr.isEmpty(); - } -} diff --git a/src/main/java/org/cqfn/astranaut/core/algorithms/mapping/TopDownAlgorithm.java b/src/main/java/org/cqfn/astranaut/core/algorithms/mapping/TopDownAlgorithm.java index 8609c81..4b8b8d8 100644 --- a/src/main/java/org/cqfn/astranaut/core/algorithms/mapping/TopDownAlgorithm.java +++ b/src/main/java/org/cqfn/astranaut/core/algorithms/mapping/TopDownAlgorithm.java @@ -100,7 +100,7 @@ boolean execute(final Node left, final Node right) { if (result) { this.ltr.put(left, right); this.rtl.put(right, left); - TopDownAlgorithm.mapSubtreesWithDifferentHashes(left, right); + this.mapSubtreesWithDifferentHashes(left, right); } } return result; @@ -137,9 +137,194 @@ private void mapSubtreesWithTheSameHash(final Node left, final Node right) { * @param left Left node * @param right Related node to the left node */ - private static void mapSubtreesWithDifferentHashes(final Node left, final Node right) { - final Unprocessed counter = new Unprocessed(left, right); - assert counter.hasUnprocessedNodes(); + private void mapSubtreesWithDifferentHashes(final Node left, final Node right) { + final Unprocessed unprocessed = new Unprocessed(left, right); + assert unprocessed.hasNodes(); + do { + if (unprocessed.onlyActionIsToInsertNodes()) { + this.insertAllNotYetMappedNodes(left, right); + break; + } + if (unprocessed.onlyActionIsToDeleteNodes()) { + this.deleteAllNotYetMappedNodes(left); + break; + } + if (this.mapTwoFirstUnmappedNodes(left, right, unprocessed)) { + continue; + } + if (this.mapTwoLastUnmappedNodes(left, right, unprocessed)) { + continue; + } + this.replaceTwoFirstUnmappedNodes(left, right, unprocessed); + } while (unprocessed.hasNodes()); + } + + /** + * Finds the first unmapped child of the left node and the first unmapped child + * of the right node and tries to map them. + * @param left Left node + * @param right Related node to the left node + * @param unprocessed Number of unprocessed nodes + * @return Mapping result, {@code true} if such nodes were found and mapped + */ + private boolean mapTwoFirstUnmappedNodes(final Node left, final Node right, + final Unprocessed unprocessed) { + final Child first = this.findFirstUnmappedChild(left); + final Child second = this.findFirstUnmappedChild(right); + boolean result; + do { + result = this.execute(first.node, second.node); + if (result) { + unprocessed.removeOnePair(); + break; + } + if (second.after != null) { + result = this.execute(first.node, second.after); + } + if (result) { + unprocessed.removeOnePair(); + final Insertion insertion = new Insertion(second.node, left, first.before); + this.inserted.add(insertion); + this.rtl.put(second.node, null); + unprocessed.nodeWasInserted(); + break; + } + if (first.after != null) { + result = this.execute(first.after, second.node); + } + if (result) { + unprocessed.removeOnePair(); + this.deleted.add(first.node); + this.ltr.put(first.node, null); + unprocessed.nodeWasDeleted(); + break; + } + } while (false); + return result; + } + + /** + * Finds the first unmapped child of the left node and the first unmapped child + * of the right node and adds a 'Replace' operation for them. + * This is a universal operation because it reduces the number of unprocessed pairs, + * and sooner or later there will be no nodes left and the algorithm will inevitably + * terminate with some result. This is fate. However, this operation may produce + * suboptimal results, and should therefore be used last. + * @param left Left node + * @param right Related node to the left node + * @param unprocessed Number of unprocessed nodes + */ + private void replaceTwoFirstUnmappedNodes(final Node left, final Node right, + final Unprocessed unprocessed) { + final Node first = this.findFirstUnmappedChild(left).node; + final Node second = this.findFirstUnmappedChild(right).node; + this.replaced.put(first, second); + this.ltr.put(first, second); + this.rtl.put(second, first); + unprocessed.removeOnePair(); + } + + /** + * Finds the first child node that has not yet been mapped. + * @param node Parent node + * @return First child node that has not yet been mapped + */ + private Child findFirstUnmappedChild(final Node node) { + final int count = node.getChildCount(); + Child result = null; + for (int index = 0; index < count; index = index + 1) { + final Node child = node.getChild(index); + if (!this.ltr.containsKey(child) && !this.rtl.containsKey(child)) { + Node before = null; + if (index > 0) { + before = node.getChild(index - 1); + } + Node after = null; + if (index < count - 1) { + after = node.getChild(index + 1); + } + result = new Child(child, before, after); + break; + } + } + assert result != null; + return result; + } + + /** + * Finds the last unmapped child of the left node and the last unmapped child + * of the right node and tries to map them. + * @param left Left node + * @param right Related node to the left node + * @param unprocessed Number of unprocessed nodes + * @return Mapping result, {@code true} if such nodes were found and mapped + */ + private boolean mapTwoLastUnmappedNodes(final Node left, final Node right, + final Unprocessed unprocessed) { + final Node first = this.findLastUnmappedChild(left); + final Node second = this.findLastUnmappedChild(right); + final boolean result = this.execute(first, second); + if (result) { + unprocessed.removeOnePair(); + } + return result; + } + + /** + * Finds the last child node that has not yet been mapped. + * @param node Parent node + * @return Last child node that has not yet been mapped + */ + private Node findLastUnmappedChild(final Node node) { + final int count = node.getChildCount(); + Node result = null; + for (int index = count - 1; index >= 0; index = index - 1) { + final Node child = node.getChild(index); + if (!this.ltr.containsKey(child) && !this.rtl.containsKey(child)) { + result = child; + break; + } + } + assert result != null; + return result; + } + + /** + * For all child nodes of the right node that are not yet mapped, performs + * the 'Insert' operation. + * @param left Left node + * @param right Related node to the left node + */ + private void insertAllNotYetMappedNodes(final Node left, final Node right) { + final int count = right.getChildCount(); + Node after = null; + for (int index = 0; index < count; index = index + 1) { + final Node node = right.getChild(index); + if (this.rtl.containsKey(node)) { + after = this.rtl.get(node); + } else { + final Insertion insertion = new Insertion(node, left, after); + this.inserted.add(insertion); + this.rtl.put(node, null); + after = node; + } + } + } + + /** + * For all child nodes of the left node that are not yet mapped, performs + * the 'Delete' operation. + * @param left Left node + */ + private void deleteAllNotYetMappedNodes(final Node left) { + final int count = left.getChildCount(); + for (int index = 0; index < count; index = index + 1) { + final Node node = left.getChild(index); + if (!this.ltr.containsKey(node)) { + this.deleted.add(node); + this.ltr.put(node, null); + } + } } /** @@ -206,12 +391,12 @@ private static class Unprocessed { /** * Number of nodes to be added. */ - private final int add; + private int add; /** * Number of nodes to be deleted. */ - private final int delete; + private int delete; /** * Constructor. @@ -229,15 +414,15 @@ private static class Unprocessed { * Checks are there still unprocessed nodes. * @return Checking result ({@code true} if yes) */ - boolean hasUnprocessedNodes() { - return this.left > 0 && this.right > 0; + boolean hasNodes() { + return this.left > 0 || this.right > 0; } /** - * Analyzes a case where the only actions that are allowed are additions. + * Analyzes a case where the only actions that are allowed are insertions. * @return Checking result, {@code true} if we can only add nodes */ - boolean onlyActionIsToAddNodes() { + boolean onlyActionIsToInsertNodes() { return this.left == 0 && this.add == this.right; } @@ -250,13 +435,80 @@ boolean onlyActionIsToDeleteNodes() { } /** - * Marks that some child of the left node has been replaced by a child of the right node. + * Notes that some child node of the right node has been recognized as an inserted node. + */ + void nodeWasInserted() { + assert this.right > 0; + this.right = this.right - 1; + if (this.add > 0) { + this.add = this.add - 1; + } else { + this.delete = this.delete + 1; + } + } + + /** + * Notes that some child node of the right node has been recognized as a deleted node. + */ + void nodeWasDeleted() { + assert this.left > 0; + this.left = this.left - 1; + if (this.delete > 0) { + this.delete = this.delete - 1; + } else { + this.add = this.add + 1; + } + } + + /** + * Marks that some child of the left node has been mapped or replaced by a child + * of the right node. */ - void nodeWasReplaced() { + void removeOnePair() { this.left = this.left - 1; this.right = this.right - 1; assert this.right >= this.add; assert this.left >= this.delete; } } + + /** + * A child node found by some criteria. + * + * @since 1.1.0 + */ + private static class Child { + /** + * Child node itself. + */ + private final Node node; + + /** + * Child node before (if exists). + */ + private final Node before; + + /** + * Child node after (if exists). + */ + private final Node after; + + /** + * Constructor. + * @param node Child node itself + * @param before Child node before + * @param after Child node after + */ + Child(final Node node, final Node before, final Node after) { + this.node = node; + this.before = before; + this.after = after; + } + + @Override + public String toString() { + return this.node.toString(); + } + } } + 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 index 78da7e5..d19ff2e 100644 --- a/src/main/java/org/cqfn/astranaut/core/algorithms/mapping/TopDownMapper.java +++ b/src/main/java/org/cqfn/astranaut/core/algorithms/mapping/TopDownMapper.java @@ -32,6 +32,17 @@ * @since 1.1.0 */ public final class TopDownMapper implements Mapper { + /** + * The instance. + */ + public static final Mapper INSTANCE = new TopDownMapper(); + + /** + * Private constructor. + */ + private TopDownMapper() { + } + @Override public Mapping map(final Node left, final Node right) { final TopDownAlgorithm algorithm = new TopDownAlgorithm(); 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 ed08a84..a5be0bf 100644 --- a/src/test/java/org/cqfn/astranaut/core/algorithms/DifferenceTreeBuilderTest.java +++ b/src/test/java/org/cqfn/astranaut/core/algorithms/DifferenceTreeBuilderTest.java @@ -24,13 +24,13 @@ package org.cqfn.astranaut.core.algorithms; import org.cqfn.astranaut.core.DifferenceNode; +import org.cqfn.astranaut.core.DraftNode; 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.algorithms.mapping.TopDownMapper; import org.cqfn.astranaut.core.example.LittleTrees; import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; /** @@ -49,7 +49,7 @@ void testTreeWithInsertedNode() { LittleTrees.createIntegerLiteral(3) ); final DifferenceTreeBuilder builder = new DifferenceTreeBuilder(before); - final boolean result = builder.build(after, new BottomUpMapper()); + final boolean result = builder.build(after, TopDownMapper.INSTANCE); Assertions.assertTrue(result); final DifferenceNode diff = builder.getRoot(); final Node expected = LittleTrees.createTreeWithInsertAction(); @@ -70,7 +70,7 @@ void testTreeWithReplacedNode() { LittleTrees.createVariable("x") ); final DifferenceTreeBuilder builder = new DifferenceTreeBuilder(before); - final boolean result = builder.build(after, new BottomUpMapper()); + final boolean result = builder.build(after, TopDownMapper.INSTANCE); Assertions.assertTrue(result); final DifferenceNode diff = builder.getRoot(); final Node expected = LittleTrees.createTreeWithReplaceAction(); @@ -93,7 +93,7 @@ void testTreeWithDeletedNode() { ); final Node after = LittleTrees.createStatementListWithTwoChildren(); final DifferenceTreeBuilder builder = new DifferenceTreeBuilder(before); - final boolean result = builder.build(after, new BottomUpMapper()); + final boolean result = builder.build(after, TopDownMapper.INSTANCE); Assertions.assertTrue(result); final DifferenceNode diff = builder.getRoot(); final Node expected = LittleTrees.createTreeWithDeleteAction(); @@ -118,7 +118,7 @@ void testTreeWithDeletedNodeInDepth() { LittleTrees.createStatementListWithTwoChildren() ); final DifferenceTreeBuilder builder = new DifferenceTreeBuilder(before); - final boolean result = builder.build(after, new BottomUpMapper()); + final boolean result = builder.build(after, TopDownMapper.INSTANCE); Assertions.assertTrue(result); final DifferenceNode diff = builder.getRoot(); final Node expected = LittleTrees.createTreeWithDeleteActionInDepth(); @@ -128,7 +128,6 @@ void testTreeWithDeletedNodeInDepth() { } @Test - @Disabled void testTreeWithReplacedNotUniqueNode() { final Node removed = LittleTrees.createIntegerLiteral(1); final Node added = LittleTrees.createIntegerLiteral(2); @@ -178,10 +177,22 @@ void testTreeWithReplacedNotUniqueNode() { Assertions.assertTrue(before.deepCompare(expected.getBefore())); Assertions.assertTrue(after.deepCompare(expected.getAfter())); final DifferenceTreeBuilder second = new DifferenceTreeBuilder(before); - second.build(after, new BottomUpMapper()); + second.build(after, TopDownMapper.INSTANCE); final DifferenceNode actual = second.getRoot(); Assertions.assertTrue(before.deepCompare(actual.getBefore())); Assertions.assertTrue(after.deepCompare(actual.getAfter())); Assertions.assertTrue(actual.deepCompare(expected)); } + + @Test + void testComplexCase() { + final Node before = DraftNode.createByDescription("X(A,B,Y(C,D,E,F,J,K))"); + final Node after = DraftNode.createByDescription("X(A,G,Y(H,C,I,E,J,K))"); + final DifferenceTreeBuilder builder = new DifferenceTreeBuilder(before); + final boolean result = builder.build(after, TopDownMapper.INSTANCE); + Assertions.assertTrue(result); + final DifferenceNode diff = builder.getRoot(); + 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 deleted file mode 100644 index dd6d2e0..0000000 --- a/src/test/java/org/cqfn/astranaut/core/algorithms/mapping/BottomUpMapperTest.java +++ /dev/null @@ -1,76 +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.Set; -import org.cqfn.astranaut.core.Node; -import org.cqfn.astranaut.core.example.LittleTrees; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; - -/** - * Test for {@link BottomUpMapper} class. - * - * @since 1.0 - */ -class BottomUpMapperTest { - /** - * Test in which the identical trees are mapped. - */ - @Test - void testIdenticalTrees() { - final Node first = LittleTrees.createTreeWithDeleteAction(); - final Node second = LittleTrees.createTreeWithDeleteAction(); - final Mapper mapper = new BottomUpMapper(); - final Mapping mapping = mapper.map(first, second); - 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( - LittleTrees.createIntegerLiteral(2) - ); - 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); - 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); - final Set deleted = mapping.getDeleted(); - Assertions.assertEquals(1, deleted.size()); - Assertions.assertEquals(first.getChild(1), deleted.iterator().next()); - } -} diff --git a/src/test/java/org/cqfn/astranaut/core/algorithms/mapping/TopDownMapperTest.java b/src/test/java/org/cqfn/astranaut/core/algorithms/mapping/TopDownMapperTest.java index 7e49599..8cafae7 100644 --- a/src/test/java/org/cqfn/astranaut/core/algorithms/mapping/TopDownMapperTest.java +++ b/src/test/java/org/cqfn/astranaut/core/algorithms/mapping/TopDownMapperTest.java @@ -23,7 +23,13 @@ */ package org.cqfn.astranaut.core.algorithms.mapping; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; import org.cqfn.astranaut.core.DraftNode; +import org.cqfn.astranaut.core.Insertion; import org.cqfn.astranaut.core.Node; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; @@ -33,15 +39,185 @@ * * @since 1.0 */ +@SuppressWarnings("PMD.TooManyMethods") class TopDownMapperTest { @Test void testIdenticalTrees() { final String description = "A(B(C, D))"; final Node first = DraftNode.createByDescription(description); final Node second = DraftNode.createByDescription(description); - final Mapper mapper = new TopDownMapper(); + final Mapper mapper = TopDownMapper.INSTANCE; final Mapping mapping = mapper.map(first, second); Assertions.assertEquals(mapping.getRight(first), second); Assertions.assertEquals(mapping.getLeft(second), first); } + + @Test + void testPairOfTreesWhereOnlyInsertion() { + final Node first = DraftNode.createByDescription("X()"); + final Node second = DraftNode.createByDescription("X(A,B)"); + final Mapper mapper = TopDownMapper.INSTANCE; + final Mapping mapping = mapper.map(first, second); + final List inserted = Arrays.asList("A", "B"); + final Set set = mapping.getInserted(); + for (final Insertion insertion : set) { + final String name = insertion.getNode().getTypeName(); + Assertions.assertTrue(inserted.contains(name)); + } + } + + @Test + void testPairOfTreesWhereOneAndOneInserted() { + final Node first = DraftNode.createByDescription("X(A)"); + final Node second = DraftNode.createByDescription("X(A,B)"); + final Mapper mapper = TopDownMapper.INSTANCE; + final Mapping mapping = mapper.map(first, second); + final Set inserted = mapping.getInserted(); + Assertions.assertEquals(1, inserted.size()); + Assertions.assertEquals("B", inserted.iterator().next().getNode().getTypeName()); + } + + @Test + void testPairOfTreesWhereTwoAndOneInserted() { + final Node first = DraftNode.createByDescription("X(A,C)"); + final Node second = DraftNode.createByDescription("X(A,B,C)"); + final Mapper mapper = TopDownMapper.INSTANCE; + final Mapping mapping = mapper.map(first, second); + final Set inserted = mapping.getInserted(); + Assertions.assertEquals(1, inserted.size()); + Assertions.assertEquals("B", inserted.iterator().next().getNode().getTypeName()); + } + + @Test + void testNodeInsertedAmongIdenticalNodes() { + final Node first = DraftNode.createByDescription("X(A,A,A,A,C)"); + final Node second = DraftNode.createByDescription("X(A,A,A,B,A,C)"); + final Mapper mapper = TopDownMapper.INSTANCE; + final Mapping mapping = mapper.map(first, second); + final Set inserted = mapping.getInserted(); + Assertions.assertEquals(1, inserted.size()); + Assertions.assertEquals("B", inserted.iterator().next().getNode().getTypeName()); + Assertions.assertEquals(second.getChild(0), mapping.getRight(first.getChild(0))); + Assertions.assertEquals(second.getChild(1), mapping.getRight(first.getChild(1))); + Assertions.assertEquals(second.getChild(2), mapping.getRight(first.getChild(2))); + Assertions.assertEquals(second.getChild(4), mapping.getRight(first.getChild(3))); + } + + @Test + void testPairOfTreesWhereTwoAndOneDeleted() { + final Node first = DraftNode.createByDescription("X(A,B)"); + final Node second = DraftNode.createByDescription("X(A)"); + final Mapper mapper = TopDownMapper.INSTANCE; + final Mapping mapping = mapper.map(first, second); + final Set deleted = mapping.getDeleted(); + Assertions.assertEquals(1, deleted.size()); + Assertions.assertEquals("B", deleted.iterator().next().getTypeName()); + } + + @Test + void testPairOfTreesWhereThreeAndOneReplaced() { + final Node first = DraftNode.createByDescription("X(A,B,C)"); + final Node second = DraftNode.createByDescription("X(A,D,C)"); + final Mapper mapper = TopDownMapper.INSTANCE; + final Mapping mapping = mapper.map(first, second); + final Map replaced = mapping.getReplaced(); + Assertions.assertEquals(1, replaced.size()); + final Map.Entry pair = replaced.entrySet().iterator().next(); + Assertions.assertEquals("B", pair.getKey().getTypeName()); + Assertions.assertEquals("D", pair.getValue().getTypeName()); + } + + @Test + void testPairOfTreesWhereOneAddedAndOneReplaced() { + final Node first = DraftNode.createByDescription("X(A,Y(C,D,E))"); + final Node second = DraftNode.createByDescription("X(A,Y(B,C,F,E))"); + final Mapper mapper = TopDownMapper.INSTANCE; + final Mapping mapping = mapper.map(first, second); + final Set added = mapping.getInserted(); + Assertions.assertEquals(1, added.size()); + Assertions.assertEquals("B", added.iterator().next().getNode().getTypeName()); + final Map replaced = mapping.getReplaced(); + Assertions.assertEquals(1, replaced.size()); + final Map.Entry pair = replaced.entrySet().iterator().next(); + Assertions.assertEquals("D", pair.getKey().getTypeName()); + Assertions.assertEquals("F", pair.getValue().getTypeName()); + } + + @Test + void testPairOfTreesWhereOneDeletedAndOneReplaced() { + final Node first = DraftNode.createByDescription("X(A,Y(B,C,D,E))"); + final Node second = DraftNode.createByDescription("X(A,Y(C,F,E))"); + final Mapper mapper = TopDownMapper.INSTANCE; + final Mapping mapping = mapper.map(first, second); + final Set deleted = mapping.getDeleted(); + Assertions.assertEquals(1, deleted.size()); + Assertions.assertEquals("B", deleted.iterator().next().getTypeName()); + final Map replaced = mapping.getReplaced(); + Assertions.assertEquals(1, replaced.size()); + final Map.Entry pair = replaced.entrySet().iterator().next(); + Assertions.assertEquals("D", pair.getKey().getTypeName()); + Assertions.assertEquals("F", pair.getValue().getTypeName()); + } + + @Test + void testPairOfTreesWhereTwoAddedAndOneReplaced() { + final Node first = DraftNode.createByDescription("X(A,Y(B,C,D))"); + final Node second = DraftNode.createByDescription("X(A,Y(B,E,F,D,F))"); + final Mapper mapper = TopDownMapper.INSTANCE; + final Mapping mapping = mapper.map(first, second); + final Set insertions = mapping.getInserted(); + Assertions.assertEquals(2, insertions.size()); + for (final Insertion insertion : insertions) { + Assertions.assertEquals("F", insertion.getNode().getTypeName()); + } + final Map replaced = mapping.getReplaced(); + Assertions.assertEquals(1, replaced.size()); + final Map.Entry pair = replaced.entrySet().iterator().next(); + Assertions.assertEquals("C", pair.getKey().getTypeName()); + Assertions.assertEquals("E", pair.getValue().getTypeName()); + } + + @Test + void testPairOfTreesWhereTwoRemovedAndOneReplaced() { + final Node first = DraftNode.createByDescription("X(A,Y(B,E,F,D,F))"); + final Node second = DraftNode.createByDescription("X(A,Y(B,C,D))"); + final Mapper mapper = TopDownMapper.INSTANCE; + final Mapping mapping = mapper.map(first, second); + final Set deletions = mapping.getDeleted(); + Assertions.assertEquals(2, deletions.size()); + for (final Node deleted : deletions) { + Assertions.assertEquals("F", deleted.getTypeName()); + } + final Map replaced = mapping.getReplaced(); + Assertions.assertEquals(1, replaced.size()); + final Map.Entry pair = replaced.entrySet().iterator().next(); + Assertions.assertEquals("E", pair.getKey().getTypeName()); + Assertions.assertEquals("C", pair.getValue().getTypeName()); + } + + @Test + void testPairOfTreesWhereAllActions() { + final Node first = DraftNode.createByDescription("X(A,B,Y(C,D,E,F,J,K))"); + final Node second = DraftNode.createByDescription("X(A,G,Y(H,C,I,E,J,K))"); + final Mapper mapper = TopDownMapper.INSTANCE; + final Mapping mapping = mapper.map(first, second); + final Set inserted = mapping.getInserted(); + Assertions.assertEquals(1, inserted.size()); + Assertions.assertEquals("H", inserted.iterator().next().getNode().getTypeName()); + final Set deleted = mapping.getDeleted(); + Assertions.assertEquals(1, deleted.size()); + Assertions.assertEquals("F", deleted.iterator().next().getTypeName()); + final Map replaced = mapping.getReplaced(); + Assertions.assertEquals(2, replaced.size()); + final Map expected = new TreeMap<>(); + expected.put("B", "G"); + expected.put("D", "I"); + for (final Map.Entry pair : replaced.entrySet()) { + Assertions.assertTrue(expected.containsKey(pair.getKey().getTypeName())); + Assertions.assertEquals( + pair.getValue().getTypeName(), + expected.get(pair.getKey().getTypeName()) + ); + } + } } diff --git a/src/test/java/org/cqfn/astranaut/core/utils/TreeVisualizerTest.java b/src/test/java/org/cqfn/astranaut/core/utils/TreeVisualizerTest.java index 65d1841..c20a56b 100644 --- a/src/test/java/org/cqfn/astranaut/core/utils/TreeVisualizerTest.java +++ b/src/test/java/org/cqfn/astranaut/core/utils/TreeVisualizerTest.java @@ -30,10 +30,8 @@ import org.cqfn.astranaut.core.DraftNode; import org.cqfn.astranaut.core.EmptyTree; import org.cqfn.astranaut.core.Node; -import org.cqfn.astranaut.core.example.LittleTrees; import org.cqfn.astranaut.core.exceptions.WrongFileExtension; import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; @@ -149,21 +147,4 @@ void testWrongExtension(@TempDir final Path temp) { } Assertions.assertTrue(oops); } - - /** - * Test for a tree visualization. - */ - @Test - @Disabled - void testActions() { - final TreeVisualizer visualizer = - new TreeVisualizer(LittleTrees.createTreeWithDeleteAction()); - boolean oops = false; - try { - visualizer.visualize(new File("X:\\syntax_tree_with_action.png")); - } catch (final WrongFileExtension | IOException exception) { - oops = true; - } - Assertions.assertFalse(oops); - } }