diff --git a/lib/functjonal-1.0-SNAPSHOT.jar b/lib/functjonal-1.0-SNAPSHOT.jar index 654612e33..7e3171051 100644 Binary files a/lib/functjonal-1.0-SNAPSHOT.jar and b/lib/functjonal-1.0-SNAPSHOT.jar differ diff --git a/local-maven-repo/org/variantsync/functjonal/1.0-SNAPSHOT/functjonal-1.0-20220705.154223-1.jar.md5 b/local-maven-repo/org/variantsync/functjonal/1.0-SNAPSHOT/functjonal-1.0-20220705.154223-1.jar.md5 deleted file mode 100644 index 8f7e849ba..000000000 --- a/local-maven-repo/org/variantsync/functjonal/1.0-SNAPSHOT/functjonal-1.0-20220705.154223-1.jar.md5 +++ /dev/null @@ -1 +0,0 @@ -b62dbf7b3ecb4116b474c02296408be1 \ No newline at end of file diff --git a/local-maven-repo/org/variantsync/functjonal/1.0-SNAPSHOT/functjonal-1.0-20220705.154223-1.jar.sha1 b/local-maven-repo/org/variantsync/functjonal/1.0-SNAPSHOT/functjonal-1.0-20220705.154223-1.jar.sha1 deleted file mode 100644 index 26c99e453..000000000 --- a/local-maven-repo/org/variantsync/functjonal/1.0-SNAPSHOT/functjonal-1.0-20220705.154223-1.jar.sha1 +++ /dev/null @@ -1 +0,0 @@ -99a3d184352a59d8b152d205ac3a4b0b3a2180d3 \ No newline at end of file diff --git a/local-maven-repo/org/variantsync/functjonal/1.0-SNAPSHOT/functjonal-1.0-20220705.154223-1.jar b/local-maven-repo/org/variantsync/functjonal/1.0-SNAPSHOT/functjonal-1.0-20221108.155446-1.jar similarity index 64% rename from local-maven-repo/org/variantsync/functjonal/1.0-SNAPSHOT/functjonal-1.0-20220705.154223-1.jar rename to local-maven-repo/org/variantsync/functjonal/1.0-SNAPSHOT/functjonal-1.0-20221108.155446-1.jar index 654612e33..7e3171051 100644 Binary files a/local-maven-repo/org/variantsync/functjonal/1.0-SNAPSHOT/functjonal-1.0-20220705.154223-1.jar and b/local-maven-repo/org/variantsync/functjonal/1.0-SNAPSHOT/functjonal-1.0-20221108.155446-1.jar differ diff --git a/local-maven-repo/org/variantsync/functjonal/1.0-SNAPSHOT/functjonal-1.0-20221108.155446-1.jar.md5 b/local-maven-repo/org/variantsync/functjonal/1.0-SNAPSHOT/functjonal-1.0-20221108.155446-1.jar.md5 new file mode 100644 index 000000000..0d80254ed --- /dev/null +++ b/local-maven-repo/org/variantsync/functjonal/1.0-SNAPSHOT/functjonal-1.0-20221108.155446-1.jar.md5 @@ -0,0 +1 @@ +1c80d4b7ba114b2380301c357e7c78e7 \ No newline at end of file diff --git a/local-maven-repo/org/variantsync/functjonal/1.0-SNAPSHOT/functjonal-1.0-20221108.155446-1.jar.sha1 b/local-maven-repo/org/variantsync/functjonal/1.0-SNAPSHOT/functjonal-1.0-20221108.155446-1.jar.sha1 new file mode 100644 index 000000000..e0781ec21 --- /dev/null +++ b/local-maven-repo/org/variantsync/functjonal/1.0-SNAPSHOT/functjonal-1.0-20221108.155446-1.jar.sha1 @@ -0,0 +1 @@ +5af42173c49ab8fca0391852570899a5c9b578c3 \ No newline at end of file diff --git a/local-maven-repo/org/variantsync/functjonal/1.0-SNAPSHOT/functjonal-1.0-20220705.154223-1.pom b/local-maven-repo/org/variantsync/functjonal/1.0-SNAPSHOT/functjonal-1.0-20221108.155446-1.pom similarity index 100% rename from local-maven-repo/org/variantsync/functjonal/1.0-SNAPSHOT/functjonal-1.0-20220705.154223-1.pom rename to local-maven-repo/org/variantsync/functjonal/1.0-SNAPSHOT/functjonal-1.0-20221108.155446-1.pom diff --git a/local-maven-repo/org/variantsync/functjonal/1.0-SNAPSHOT/functjonal-1.0-20220705.154223-1.pom.md5 b/local-maven-repo/org/variantsync/functjonal/1.0-SNAPSHOT/functjonal-1.0-20221108.155446-1.pom.md5 similarity index 100% rename from local-maven-repo/org/variantsync/functjonal/1.0-SNAPSHOT/functjonal-1.0-20220705.154223-1.pom.md5 rename to local-maven-repo/org/variantsync/functjonal/1.0-SNAPSHOT/functjonal-1.0-20221108.155446-1.pom.md5 diff --git a/local-maven-repo/org/variantsync/functjonal/1.0-SNAPSHOT/functjonal-1.0-20220705.154223-1.pom.sha1 b/local-maven-repo/org/variantsync/functjonal/1.0-SNAPSHOT/functjonal-1.0-20221108.155446-1.pom.sha1 similarity index 100% rename from local-maven-repo/org/variantsync/functjonal/1.0-SNAPSHOT/functjonal-1.0-20220705.154223-1.pom.sha1 rename to local-maven-repo/org/variantsync/functjonal/1.0-SNAPSHOT/functjonal-1.0-20221108.155446-1.pom.sha1 diff --git a/local-maven-repo/org/variantsync/functjonal/1.0-SNAPSHOT/maven-metadata.xml b/local-maven-repo/org/variantsync/functjonal/1.0-SNAPSHOT/maven-metadata.xml index 0eef12e7d..f60d63722 100644 --- a/local-maven-repo/org/variantsync/functjonal/1.0-SNAPSHOT/maven-metadata.xml +++ b/local-maven-repo/org/variantsync/functjonal/1.0-SNAPSHOT/maven-metadata.xml @@ -2,24 +2,24 @@ org.variantsync functjonal - 1.0-SNAPSHOT + 20221108155446 - 20220705.154223 + 20221108.155446 1 - 20220705154223 jar - 1.0-20220705.154223-1 - 20220705154223 + 1.0-20221108.155446-1 + 20221108155446 pom - 1.0-20220705.154223-1 - 20220705154223 + 1.0-20221108.155446-1 + 20221108155446 + 1.0-SNAPSHOT diff --git a/local-maven-repo/org/variantsync/functjonal/1.0-SNAPSHOT/maven-metadata.xml.md5 b/local-maven-repo/org/variantsync/functjonal/1.0-SNAPSHOT/maven-metadata.xml.md5 index 57fa035c3..4148ee479 100644 --- a/local-maven-repo/org/variantsync/functjonal/1.0-SNAPSHOT/maven-metadata.xml.md5 +++ b/local-maven-repo/org/variantsync/functjonal/1.0-SNAPSHOT/maven-metadata.xml.md5 @@ -1 +1 @@ -970fd829eb10a39db3fac3f66239d6d5 \ No newline at end of file +8e0dc18f4c0caf15829984d35e21b416 \ No newline at end of file diff --git a/local-maven-repo/org/variantsync/functjonal/1.0-SNAPSHOT/maven-metadata.xml.sha1 b/local-maven-repo/org/variantsync/functjonal/1.0-SNAPSHOT/maven-metadata.xml.sha1 index 3a43dafc4..5cd70e2a6 100644 --- a/local-maven-repo/org/variantsync/functjonal/1.0-SNAPSHOT/maven-metadata.xml.sha1 +++ b/local-maven-repo/org/variantsync/functjonal/1.0-SNAPSHOT/maven-metadata.xml.sha1 @@ -1 +1 @@ -9dc50711cb637ba6126e6027dc96661db8def5f5 \ No newline at end of file +a6dd456d37dde01239c14423e5d44becb5e56936 \ No newline at end of file diff --git a/local-maven-repo/org/variantsync/functjonal/maven-metadata.xml b/local-maven-repo/org/variantsync/functjonal/maven-metadata.xml index 209704c7e..b6b9977d1 100644 --- a/local-maven-repo/org/variantsync/functjonal/maven-metadata.xml +++ b/local-maven-repo/org/variantsync/functjonal/maven-metadata.xml @@ -6,6 +6,6 @@ 1.0-SNAPSHOT - 20220705154223 + 20221108155446 diff --git a/local-maven-repo/org/variantsync/functjonal/maven-metadata.xml.md5 b/local-maven-repo/org/variantsync/functjonal/maven-metadata.xml.md5 index ed7e63873..39b7fc6a8 100644 --- a/local-maven-repo/org/variantsync/functjonal/maven-metadata.xml.md5 +++ b/local-maven-repo/org/variantsync/functjonal/maven-metadata.xml.md5 @@ -1 +1 @@ -3cfa20d6ed954ba0787eacf0781edfd6 \ No newline at end of file +3f5fd991f33dcff72c2518ba693e3f6f \ No newline at end of file diff --git a/local-maven-repo/org/variantsync/functjonal/maven-metadata.xml.sha1 b/local-maven-repo/org/variantsync/functjonal/maven-metadata.xml.sha1 index e19f9959e..cd6abebd6 100644 --- a/local-maven-repo/org/variantsync/functjonal/maven-metadata.xml.sha1 +++ b/local-maven-repo/org/variantsync/functjonal/maven-metadata.xml.sha1 @@ -1 +1 @@ -41d1da59b5cda4773b7c2eaddb4e5c0a0c1c77d8 \ No newline at end of file +2cd6cf429f095990ef3c6e68237aea734a7771e6 \ No newline at end of file diff --git a/src/main/java/org/variantsync/diffdetective/diff/DiffLineNumber.java b/src/main/java/org/variantsync/diffdetective/diff/DiffLineNumber.java index bd103243f..fbb48caae 100644 --- a/src/main/java/org/variantsync/diffdetective/diff/DiffLineNumber.java +++ b/src/main/java/org/variantsync/diffdetective/diff/DiffLineNumber.java @@ -1,6 +1,7 @@ package org.variantsync.diffdetective.diff; import org.variantsync.diffdetective.diff.difftree.DiffType; +import org.variantsync.diffdetective.diff.difftree.Time; import java.util.Objects; @@ -39,6 +40,22 @@ public static DiffLineNumber Invalid() { return new DiffLineNumber(InvalidLineNumber, InvalidLineNumber, InvalidLineNumber); } + public DiffLineNumber withLineNumberAtTime(int lineNumber, Time time) { + return new DiffLineNumber( + inDiff, + time.match(lineNumber, beforeEdit), + time.match(afterEdit, lineNumber) + ); + } + + public DiffLineNumber withLineNumberInDiff(int lineNumber) { + return new DiffLineNumber( + lineNumber, + beforeEdit, + afterEdit + ); + } + /** * Shifts this line number by adding the given offset. * @param offset value to add to this line number. @@ -77,6 +94,15 @@ public DiffLineNumber as(final DiffType diffType) { ); } + /** + * Returns the line number at the given time. + * @param time the time at which to return the line range + * @return {@code beforeEdit} or {@code afterEdit}, depending on {@code time} + */ + public int atTime(Time time) { + return time.match(beforeEdit, afterEdit); + } + @Override public String toString() { return "(old: " + beforeEdit + ", diff: " + inDiff + ", new:" + afterEdit + ")"; @@ -107,24 +133,14 @@ public static Lines rangeInDiff(final DiffLineNumber from, final DiffLineNumber } /** - * Returns the range between two line numbers before the edit. - * @see DiffLineNumber#inDiff - * @param from The start line number. - * @param to The end line number. - * @return [from.beforeEdit, to.beforeEdit) - */ - public static Lines rangeBeforeEdit(final DiffLineNumber from, final DiffLineNumber to) { - return Lines.FromInclToExcl(from.beforeEdit, to.beforeEdit); - } - - /** - * Returns the range between two line numbers before the edit. + * Returns the range between two line numbers at a given time. * @see DiffLineNumber#inDiff * @param from The start line number. * @param to The end line number. - * @return [from.afterEdit, to.afterEdit) + * @param time The time at which to return the line range. + * @return [from.beforeEdit, to.beforeEdit) or [from.afterEdit, to.afterEdit) */ - public static Lines rangeAfterEdit(final DiffLineNumber from, final DiffLineNumber to) { - return Lines.FromInclToExcl(from.afterEdit, to.afterEdit); + public static Lines rangeAtTime(final DiffLineNumber from, final DiffLineNumber to, Time time) { + return Lines.FromInclToExcl(from.atTime(time), to.atTime(time)); } } diff --git a/src/main/java/org/variantsync/diffdetective/diff/difftree/DiffGraph.java b/src/main/java/org/variantsync/diffdetective/diff/difftree/DiffGraph.java index 94709153d..40068d658 100644 --- a/src/main/java/org/variantsync/diffdetective/diff/difftree/DiffGraph.java +++ b/src/main/java/org/variantsync/diffdetective/diff/difftree/DiffGraph.java @@ -2,6 +2,9 @@ import java.util.Collection; +import static org.variantsync.diffdetective.diff.difftree.Time.AFTER; +import static org.variantsync.diffdetective.diff.difftree.Time.BEFORE; + /** * Generalisation of DiffTrees to arbitrary change graphs with variability information. * The DiffGraph class currently does not model a graph itself but rather @@ -33,8 +36,8 @@ public static DiffTree fromNodes(final Collection nodes, final DiffTre .filter(DiffNode::isRoot) .forEach(n -> n.diffType.matchBeforeAfter( - () -> newRoot.addBeforeChild(n), - () -> newRoot.addAfterChild(n) + () -> newRoot.addChild(n, BEFORE), + () -> newRoot.addChild(n, AFTER) )); return new DiffTree(newRoot, source); } diff --git a/src/main/java/org/variantsync/diffdetective/diff/difftree/DiffNode.java b/src/main/java/org/variantsync/diffdetective/diff/difftree/DiffNode.java index 4d16761b1..b7e72e9e6 100644 --- a/src/main/java/org/variantsync/diffdetective/diff/difftree/DiffNode.java +++ b/src/main/java/org/variantsync/diffdetective/diff/difftree/DiffNode.java @@ -1,18 +1,19 @@ package org.variantsync.diffdetective.diff.difftree; -import org.prop4j.And; import org.prop4j.Node; import org.variantsync.diffdetective.diff.DiffLineNumber; import org.variantsync.diffdetective.diff.Lines; import org.variantsync.diffdetective.util.Assert; import org.variantsync.diffdetective.util.StringUtils; import org.variantsync.diffdetective.util.fide.FixTrueFalse; +import org.variantsync.diffdetective.variationtree.HasNodeType; +import org.variantsync.diffdetective.variationtree.VariationNode; import java.util.*; -import java.util.function.Function; import java.util.stream.Collectors; -import static org.variantsync.diffdetective.util.fide.FormulaUtils.negate; +import static org.variantsync.diffdetective.diff.difftree.Time.AFTER; +import static org.variantsync.diffdetective.diff.difftree.Time.BEFORE; /** * Implementation of a node in a {@link DiffTree}. @@ -23,9 +24,7 @@ * DiffNode's store parent and child information to build a graph. * @author Paul Bittner, Sören Viegener, Benjamin Moosherr */ -public class DiffNode { - private static final short ID_OFFSET = 3; - +public class DiffNode implements HasNodeType { /** * The diff type of this node, which determines if this node represents * an inserted, removed, or unchanged element in a diff. @@ -45,32 +44,35 @@ public class DiffNode { private List lines; /** - * The parent {@link DiffNode} before the edit. - * - * Invariant: Iff {@code beforeParent != null} then - * {@code beforeParent.childOrder.contains(this)}. - */ - private DiffNode beforeParent; - - /** - * The parent {@link DiffNode} after the edit. + * The parents {@link DiffNode} before and after the edit. + * This array has to be indexed by {@code Time.ordinal()} * - * Invariant: Iff {@code afterParent != null} then - * {@code afterParent.childOrder.contains(this)}. + * Invariant: Iff {@code getParent(time) != null} then + * {@code getParent(time).childOrder.contains(this)}. */ - private DiffNode afterParent; + private DiffNode[] parents = new DiffNode[2]; /** * We use a list for children to maintain order. * * Invariant: Iff {@code childOrder.contains(child)} then - * {@code child.beforeParent == this || child.afterParent == this}. + * {@code child.getParent(BEFORE) == this || child.getParent(AFTER) == this}. * * Note that it's explicitly allowed to have - * {@code child.beforeParent == this && child.afterParent == this}. + * {@code child.getParent(BEFORE) == this && child.getParent(AFTER) == this}. */ private final List childOrder; + /** + * Cache for before and after projections. + * It stores the projection node at each time so that only one instance of {@link Projection} + * per {@link Time} is ever created. This array has to be indexed by {@code Time.ordinal()} + * + *

This field is required to allow identity tests of {@link Projection}s with {@code ==} instead + * of {@link Projection#isSameAs}. + */ + private Projection[] projections = new Projection[2]; + /** * Creates a DiffNode with the given parameters. * @param diffType The type of change made to this node. @@ -146,14 +148,15 @@ public void addLines(final List lines) { /** * Returns the lines in the diff that are represented by this DiffNode. + * The returned list is unmodifiable. */ - public List getLines() { - return lines; + public List getLabelLines() { + return Collections.unmodifiableList(lines); } /** * Returns the lines in the diff that are represented by this DiffNode as a single text. - * @see DiffNode#getLines + * @see DiffNode#getLabelLines */ public String getLabel() { return String.join(StringUtils.LINEBREAK, lines); @@ -169,87 +172,20 @@ public void setLabel(String label) { } /** - * Gets the first if node in the path following the before parent - * @return The first if node in the path following the before parent - */ - public DiffNode getBeforeIfNode() { - if (isIf()) { - return this; - } - if (isRoot()) { - return null; - } - return beforeParent.getBeforeIfNode(); - } - - /** - * Gets the first if node in the path following the after parent - * @return The first if node in the path following the after parent - */ - public DiffNode getAfterIfNode() { - if (isIf()) { - return this; - } - if (isRoot()) { - return null; - } - return afterParent.getAfterIfNode(); - } - - /** - * Gets the depth of the diff tree following the before parent - * @return the depth of the diff tree following the before parent - */ - public int getBeforeAnnotationDepth(){ - if (isRoot()) { - return 0; - } - - if (isIf()) { - return beforeParent.getBeforeAnnotationDepth() + 1; - } - - return beforeParent.getBeforeAnnotationDepth(); - } - - /** - * Gets the depth of the diff tree following the after parent - * @return the depth of the diff tree following the after parent - */ - public int getAfterAnnotationDepth(){ - if (isRoot()) { - return 0; - } - - if (isIf()) { - return afterParent.getAfterAnnotationDepth() + 1; - } - - return afterParent.getAfterAnnotationDepth(); - } - - /** - * Gets the depth of the diff tree following the before parent - * @return the depth of the diff tree following the before parent + * Gets the first {@code if} node in the path from the root to this node at the time + * {@code time}. + * @return The first {@code if} node in the path to the root at the time {@code time} */ - public int getBeforeDepth(){ - if (isRoot()) { - return 0; - } - - return beforeParent.getBeforeDepth() + 1; + public DiffNode getIfNode(Time time) { + return projection(time).getIfNode().getBackingNode(); } /** - * Gets the depth of the diff tree following the after parent - * @return the depth of the diff tree following the after parent + * Gets the length of the path from the root to this node at the time {@code time}. + * @return the depth of the this node in the diff tree at the time {@code time} */ - public int getAfterDepth(){ - if (isRoot()) { - return 0; - } - - return afterParent.getAfterDepth() + 1; + public int getDepth(Time time) { + return projection(time).getDepth(); } /** @@ -257,13 +193,13 @@ public int getAfterDepth(){ * are the very same. */ public boolean beforePathEqualsAfterPath() { - if (beforeParent == afterParent) { - if (beforeParent == null) { + if (getParent(BEFORE) == getParent(AFTER)) { + if (getParent(BEFORE) == null) { // root return true; } - return beforeParent.beforePathEqualsAfterPath(); + return getParent(BEFORE).beforePathEqualsAfterPath(); } return false; @@ -277,63 +213,40 @@ public int getTotalNumberOfChildren() { } /** - * Gets the amount of nodes with diff type REM in the path following the before parent - * @return the amount of nodes with diff type REM in the path following the before parent + * Gets the amount of nodes on the path from the root to this node which only exist at the time + * {@code time}. */ - public int getRemAmount() { + public int getChangeAmount(Time time) { if (isRoot()) { return 0; } - if (isIf() && diffType.equals(DiffType.REM)) { - return beforeParent.getRemAmount() + 1; + var changeType = DiffType.thatExistsOnlyAt(time); + + if (isIf() && diffType.equals(changeType)) { + return getParent(time).getChangeAmount(time) + 1; } - if ((isElif() || isElse()) && diffType.equals(DiffType.REM)) { + if ((isElif() || isElse()) && diffType.equals(changeType)) { // if this is a removed elif or else we do not want to count the other branches of // this annotation // we thus go up the tree until we get the next if and continue with the parent of it - return beforeParent.getBeforeIfNode().beforeParent.getRemAmount() + 1; + return getParent(time).getIfNode(time).getParent(time).getChangeAmount(time) + 1; } - return beforeParent.getRemAmount(); + return getParent(time).getChangeAmount(time); } /** - * Gets the amount of nodes with diff type ADD in the path following the after parent - * @return the amount of nodes with diff type ADD in the path following the after parent + * Sets the parent at {@code time} checking that this node doesn't currently have a parent. */ - public int getAddAmount() { - if (isRoot()) { - return 0; - } - - if (isIf() && diffType.equals(DiffType.ADD)) { - return afterParent.getAddAmount() + 1; - } - - if ((isElif() || isElse()) && diffType.equals(DiffType.ADD)) { - // if this is an added elif or else we do not want to count the other branches of - // this annotation - // we thus go up the tree until we get the next if and continue with the parent of it - return afterParent.getAfterIfNode().afterParent.getAddAmount() + 1; - } - - return afterParent.getAddAmount(); - } - - private void setBeforeParent(final DiffNode newBeforeParent) { - Assert.assertTrue(beforeParent == null); - this.beforeParent = newBeforeParent; - } - - private void setAfterParent(final DiffNode newAfterParent) { - Assert.assertTrue(afterParent == null); - this.afterParent = newAfterParent; + private void setParent(final DiffNode newParent, Time time) { + Assert.assertTrue(getParent(time) == null); + parents[time.ordinal()] = newParent; } /** - * Adds thus subtree below the given parents. + * Adds this subtree below the given parents. * Inverse of drop. * @param newBeforeParent Node that should be this node's before parent. May be null. * @param newAfterParent Node that should be this node's after parent. May be null. @@ -342,10 +255,10 @@ private void setAfterParent(final DiffNode newAfterParent) { public boolean addBelow(final DiffNode newBeforeParent, final DiffNode newAfterParent) { boolean success = false; if (newBeforeParent != null) { - success |= newBeforeParent.addBeforeChild(this); + success |= newBeforeParent.addChild(this, BEFORE); } if (newAfterParent != null) { - success |= newAfterParent.addAfterChild(this); + success |= newAfterParent.addChild(this, AFTER); } return success; } @@ -355,22 +268,19 @@ public boolean addBelow(final DiffNode newBeforeParent, final DiffNode newAfterP * Inverse of addBelow. */ public void drop() { - if (beforeParent != null) { - beforeParent.removeBeforeChild(this); - } - if (afterParent != null) { - afterParent.removeAfterChild(this); - } - } - - private void dropBeforeChild(final DiffNode child) { - Assert.assertTrue(child.beforeParent == this); - child.beforeParent = null; + Time.forAll(time -> { + if (getParent(time) != null) { + getParent(time).removeChild(this, time); + } + }); } - private void dropAfterChild(final DiffNode child) { - Assert.assertTrue(child.afterParent == this); - child.afterParent = null; + /** + * Remove this node as the parent of {@code child} but it doesn't change {@link childOrder}. + */ + private void dropChild(final DiffNode child, Time time) { + Assert.assertTrue(child.getParent(time) == this); + child.parents[time.ordinal()] = null; } /** @@ -382,132 +292,59 @@ public int indexOfChild(final DiffNode child) { } /** - * Adds the given node for the given time at the given index as the child. - * @param child The new child to add. This node should not be a child of another node for the given time. - * @param index The index at which the node should be inserted into the children list. - * @param time The time at which this node should be the parent of this node. - * For example, if the time is BEFORE, then this node will become the before parent of the given node. - * @return True iff the insertion was successful. False iff the child could not be added. - * @see DiffNode#insertBeforeChild - * @see DiffNode#insertAfterChild - */ - public boolean insertChildAt(final DiffNode child, int index, Time time) { - return switch (time) { - case BEFORE -> insertBeforeChild(child, index); - case AFTER -> insertAfterChild(child, index); - }; - } - - /** - * The same as {@link DiffNode#insertChildAt} but the time fixed to BEFORE. - */ - public boolean insertBeforeChild(final DiffNode child, int index) { - if (!child.isAdd()) { - if (!isChild(child)) { - childOrder.add(index, child); - } - child.setBeforeParent(this); - return true; - } - return false; - } - - /** - * The same as {@link DiffNode#insertChildAt} but the time fixed to AFTER. + * Insert {@code child} as child at the time {@code time} at the position {@code index}. */ - public boolean insertAfterChild(final DiffNode child, int index) { - if (!child.isRem()) { + public boolean insertChild(final DiffNode child, int index, Time time) { + if (child.getDiffType().existsAtTime(time)) { if (!isChild(child)) { childOrder.add(index, child); } - child.setAfterParent(this); + child.setParent(this, time); return true; } return false; } /** - * The same as {@link DiffNode#insertBeforeChild} but puts the node at the end of the children + * The same as {@link DiffNode#insertChild} but puts the node at the end of the children * list instead of inserting it at a specific index. */ - public boolean addBeforeChild(final DiffNode child) { - if (!child.isAdd()) { - if (child.beforeParent != null) { - throw new IllegalArgumentException("Given child " + child + " already has a before parent (" + child.beforeParent + ")!"); + public boolean addChild(final DiffNode child, Time time) { + if (child.getDiffType().existsAtTime(time)) { + if (child.getParent(time) != null) { + throw new IllegalArgumentException("Given child " + child + " already has a before parent (" + child.getParent(time) + ")!"); } if (!isChild(child)) { childOrder.add(child); } - child.setBeforeParent(this); + child.setParent(this, time); return true; } return false; } /** - * The same as {@link DiffNode#insertAfterChild} but puts the node at the end of the children - * list instead of inserting it at a specific index. + * Adds all given nodes at the time {@code time} as children using {@link DiffNode#addChild}. + * @param children Nodes to add as children. + * @param time whether to add {@code children} before or after the edit */ - public boolean addAfterChild(final DiffNode child) { - if (!child.isRem()) { - if (child.afterParent != null) { - throw new IllegalArgumentException("Given child " + child + " already has an after parent (" + child.afterParent + ")!"); - } - - if (!isChild(child)) { - childOrder.add(child); - } - child.setAfterParent(this); - return true; + public void addChildren(final Collection children, Time time) { + for (final DiffNode child : children) { + addChild(child, time); } - return false; } /** - * Adds all given nodes as before children using {@link DiffNode#addBeforeChild}. - * @param beforeChildren Nodes to add as children before the edit. + * Removes the given node from this node's children before or after the edit. + * The node might still remain a child after or before the edit. + * @param child the child to remove + * @param time whether {@code child} should be removed before or after the edit + * @return True iff the child was removed, false iff it's not a child at {@code time}. */ - public void addBeforeChildren(final Collection beforeChildren) { - for (final DiffNode beforeChild : beforeChildren) { - addBeforeChild(beforeChild); - } - } - - /** - * Adds all given nodes as after children using {@link DiffNode#addAfterChild}. - * @param afterChildren Nodes to add as children after the edit. - */ - public void addAfterChildren(final Collection afterChildren) { - for (final DiffNode afterChild : afterChildren) { - addAfterChild(afterChild); - } - } - - /** - * Removes the given node from this node's children before the edit. - * The node might still remain a child after the edit. - * @param child The child to remove before the edit. - * @return True iff the child was removed, false iff it was no before child. - */ - public boolean removeBeforeChild(final DiffNode child) { - if (isBeforeChild(child)) { - dropBeforeChild(child); - removeFromCache(child); - return true; - } - return false; - } - - /** - * Removes the given node from this node's children after the edit. - * The node might still remain a child before the edit. - * @param child The child to remove after the edit. - * @return True iff the child was removed, false iff it was no after child. - */ - public boolean removeAfterChild(final DiffNode child) { - if (isAfterChild(child)) { - dropAfterChild(child); + public boolean removeChild(final DiffNode child, Time time) { + if (isChild(child, time)) { + dropChild(child, time); removeFromCache(child); return true; } @@ -521,53 +358,29 @@ public boolean removeAfterChild(final DiffNode child) { */ public void removeChildren(final Collection childrenToRemove) { for (final DiffNode childToRemove : childrenToRemove) { - removeBeforeChild(childToRemove); - removeAfterChild(childToRemove); + Time.forAll(time -> removeChild(childToRemove, time)); } } /** - * Removes all children before the edit. - * Afterwards, this node will have no before children. - * @return All removed before children. + * Removes all children before or after the edit. + * Afterwards, this node will have no children at the given time. + * @param time whether to remove all children before or after the edit + * @return All removed children. */ - public List removeBeforeChildren() { + public List removeChildren(Time time) { final List orphans = new ArrayList<>(); // Note that the following method call can't be written using a foreach loop reusing // {@code removeBeforeChild} because lists can't be modified during traversal. childOrder.removeIf(child -> { - if (!isBeforeChild(child)) { - return false; - } - - orphans.add(child); - dropBeforeChild(child); - return !isAfterChild(child); - }); - - return orphans; - } - - - /** - * Removes all children after the edit. - * Afterwards, this node will have no after children. - * @return All removed after children. - */ - public List removeAfterChildren() { - final List orphans = new ArrayList<>(); - - // Note that the following method call can't be written using a foreach loop reusing - // {@code removeAfterChild} because lists can't be modified during traversal. - childOrder.removeIf(child -> { - if (!isAfterChild(child)) { + if (!isChild(child, time)) { return false; } orphans.add(child); - dropAfterChild(child); - return !isBeforeChild(child); + dropChild(child, time); + return !isChild(child, time.other()); }); return orphans; @@ -593,22 +406,14 @@ private void removeFromCache(final DiffNode child) { * @param other The node whose children should be stolen. */ public void stealChildrenOf(final DiffNode other) { - addBeforeChildren(other.removeBeforeChildren()); - addAfterChildren(other.removeAfterChildren()); - } - - /** - * Returns the parent of this node before the edit. - */ - public DiffNode getBeforeParent() { - return beforeParent; + Time.forAll(time -> addChildren(other.removeChildren(time), time)); } /** - * Returns the parent of this node after the edit. + * Returns the parent of this node before or after the edit. */ - public DiffNode getAfterParent() { - return afterParent; + public DiffNode getParent(Time time) { + return parents[time.ordinal()]; } /** @@ -643,19 +448,20 @@ public Lines getLinesInDiff() { } /** - * Returns the range of line numbers of this node's corresponding source code before the edit. - * @see DiffLineNumber#rangeBeforeEdit + * Returns the range of line numbers of this node's corresponding source code before or after + * the edit. */ - public Lines getLinesBeforeEdit() { - return DiffLineNumber.rangeBeforeEdit(from, to); + public Lines getLinesAtTime(Time time) { + return DiffLineNumber.rangeAtTime(from, to, time); } /** - * Returns the range of line numbers of this node's corresponding source code after the edit. - * @see DiffLineNumber#rangeAfterEdit + * Returns the range of line numbers of this node's corresponding source code before or after + * the edit. */ - public Lines getLinesAfterEdit() { - return DiffLineNumber.rangeAfterEdit(from, to); + public void setLinesAtTime(Lines lines, Time time) { + from = from.withLineNumberAtTime(lines.getFromInclusive(), time); + to = to.withLineNumberAtTime(lines.getToExclusive(), time); } /** @@ -689,197 +495,35 @@ public List getAllChildren() { * The feature mapping of {@link NodeType#ELSE} and {@link NodeType#ELIF} nodes is determined by all formulas in the respective if-elif-else chain. * The feature mapping of an {@link NodeType#ARTIFACT artifact} node is the feature mapping of its parent. * See Equation (1) in our paper (+ its extension to time for variation tree diffs described in Section 3.1). - * @param parentOf Function that returns the parent of a node. - * This function decides whether the before or after parent should be visited. - * It thus decides whether to compute the feature mapping before or after the edit. - * @return The feature mapping of this node for the given parent edges. - * The returned list represents a conjunction (i.e., all clauses should be combined with boolean AND). - */ - private List getFeatureMappingClauses(final Function parentOf) { - final DiffNode parent = parentOf.apply(this); - - if (isElse() || isElif()) { - List and = new ArrayList<>(); - - if (isElif()) { - and.add(getDirectFeatureMapping()); - } - - // Negate all previous cases - DiffNode ancestor = parent; - while (!ancestor.isIf()) { - if (ancestor.isElif()) { - and.add(negate(ancestor.getDirectFeatureMapping())); - } else { - throw new RuntimeException("Expected If or Elif above Else or Elif but got " + ancestor.nodeType + " from " + ancestor); - // Assert.assertTrue(ancestor.isArtifact()); - } - ancestor = parentOf.apply(ancestor); - } - and.add(negate(ancestor.getDirectFeatureMapping())); - - return and; - } else if (isArtifact()) { - return parent.getFeatureMappingClauses(parentOf); - } - - return List.of(getDirectFeatureMapping()); - } - - /** - * Same as {@link DiffNode#getFeatureMappingClauses} but conjuncts the returned clauses to a single formula. - */ - private Node getFeatureMapping(Function parentOf) { - final List fmClauses = getFeatureMappingClauses(parentOf); - if (fmClauses.size() == 1) { - return fmClauses.get(0); - } - return new And(fmClauses); - } - - /** - * Returns the full feature mapping formula of this node before the edit. - * The feature mapping of an {@link NodeType#IF} node is its {@link DiffNode#getDirectFeatureMapping direct feature mapping}. - * The feature mapping of {@link NodeType#ELSE} and {@link NodeType#ELIF} nodes is determined by all formulas in the respective if-elif-else chain. - * The feature mapping of an {@link NodeType#ARTIFACT artifact} node is the feature mapping of its parent. - * See Equation (1) in our paper (+ its extension to time for variation tree diffs described in Section 3.1). - * @return The feature mapping of this node for the given parent edges. - */ - public Node getBeforeFeatureMapping() { - return getFeatureMapping(DiffNode::getBeforeParent); - } - - /** - * Returns the full feature mapping formula of this node after the edit. - * The feature mapping of an {@link NodeType#IF} node is its {@link DiffNode#getDirectFeatureMapping direct feature mapping}. - * The feature mapping of {@link NodeType#ELSE} and {@link NodeType#ELIF} nodes is determined by all formulas in the respective if-elif-else chain. - * The feature mapping of an {@link NodeType#ARTIFACT artifact} node is the feature mapping of its parent. - * See Equation (1) in our paper (+ its extension to time for variation tree diffs described in Section 3.1). + * @param time Whether to return the feature mapping clauses before or after the edit. * @return The feature mapping of this node for the given parent edges. */ - public Node getAfterFeatureMapping() { - return getFeatureMapping(DiffNode::getAfterParent); - } - - /** - * Depending on the given time, returns either the - * {@link DiffNode#getBeforeFeatureMapping() before feature mapping} or - * {@link DiffNode#getAfterFeatureMapping() after feature mapping}. - */ public Node getFeatureMapping(Time time) { - return time.match( - this::getBeforeFeatureMapping, - this::getAfterFeatureMapping - ); - } - - /** - * Returns the presence condition of this node for the respective time. - * See Equation (2) in our paper (+ its extension to time for variation tree diffs described in Section 3.1). - * @param parentOf Function that returns the parent of a node. - * This function decides whether the before or after parent should be visited. - * It thus decides whether to compute the feature mapping before or after the edit. - * @return The presence condition of this node for the given parent edges. - * The returned list represents a conjunction (i.e., all clauses should be combined with boolean AND). - */ - private List getPresenceCondition(Function parentOf) { - final DiffNode parent = parentOf.apply(this); - - if (isElse() || isElif()) { - final List clauses = new ArrayList<>(getFeatureMappingClauses(parentOf)); - - // Find corresponding if - DiffNode correspondingIf = parent; - while (!correspondingIf.isIf()) { - correspondingIf = parentOf.apply(correspondingIf); - } - - // If this elif-else-chain was again nested in another annotation, add its pc. - final DiffNode outerNesting = parentOf.apply(correspondingIf); - if (outerNesting != null) { - clauses.addAll(outerNesting.getPresenceCondition(parentOf)); - } - - return clauses; - } else if (isArtifact()) { - return parent.getPresenceCondition(parentOf); - } - - // this is mapping or root - final List clauses; - if (parent == null) { - clauses = new ArrayList<>(1); - } else { - clauses = parent.getPresenceCondition(parentOf); - } - clauses.add(featureMapping); - return clauses; + return projection(time).getFeatureMapping(); } /** - * Returns the presence condition of this node before the edit. + * Returns the presence condition of this node before or after the edit. * See Equation (2) in our paper (+ its extension to time for variation tree diffs described in Section 3.1). + * @param time Whether to return the presence condition before or after the edit. * @return The presence condition of this node for the given parent edges. */ - public Node getBeforePresenceCondition() { - if (diffType.existsBefore()) { - return new And(getPresenceCondition(DiffNode::getBeforeParent)); - } else { - throw new WrongTimeException("Cannot determine before PC of added node " + this); - } - } - - /** - * Returns the presence condition of this node after the edit. - * See Equation (2) in our paper (+ its extension to time for variation tree diffs described in Section 3.1). - * @return The presence condition of this node for the given parent edges. - */ - public Node getAfterPresenceCondition() { - if (diffType.existsAfter()) { - return new And(getPresenceCondition(DiffNode::getAfterParent)); - } else { - throw new WrongTimeException("Cannot determine after PC of removed node " + this); - } - } - - /** - * Depending on the given time, returns either the - * {@link DiffNode#getBeforePresenceCondition() before presence condition} or - * {@link DiffNode#getAfterPresenceCondition() after presence condition}. - */ public Node getPresenceCondition(Time time) { - return time.match( - this::getBeforePresenceCondition, - this::getAfterPresenceCondition - ); - } - - /** - * Returns true iff this node is the before parent of the given node. - */ - public boolean isBeforeChild(DiffNode child) { - return child.beforeParent == this; - } - - /** - * Returns true iff this node is the after parent of the given node. - */ - public boolean isAfterChild(DiffNode child) { - return child.afterParent == this; + return projection(time).getPresenceCondition(); } /** * Returns true iff this node is the before or after parent of the given node. */ public boolean isChild(DiffNode child) { - return isBeforeChild(child) || isAfterChild(child); + return isChild(child, BEFORE) || isChild(child, AFTER); } /** * Returns true iff this node is the parent of the given node at the given time. */ public boolean isChild(DiffNode child, Time time) { - return time.match(isBeforeChild(child), isAfterChild(child)); + return child.getParent(time) == this; } /** @@ -920,50 +564,16 @@ public DiffType getDiffType() { return this.diffType; } - /** - * Returns true if this node represents an ELIF annotation. - * @see NodeType#ELIF - */ - public boolean isElif() { - return this.nodeType.equals(NodeType.ELIF); - } - - /** - * Returns true if this node represents a conditional annotation. - * @see NodeType#IF - */ - public boolean isIf() { - return this.nodeType.equals(NodeType.IF); - } - - /** - * Returns true if this node is an artifact node. - * @see NodeType#ARTIFACT - */ - public boolean isArtifact() { - return this.nodeType.equals(NodeType.ARTIFACT); - } - - /** - * Returns true if this node represents an ELSE annotation. - * @see NodeType#ELSE - */ - public boolean isElse() { - return this.nodeType.equals(NodeType.ELSE); + @Override + public NodeType getNodeType() { + return nodeType; } /** * Returns true if this node is a root node (has no parents). */ public boolean isRoot() { - return getBeforeParent() == null && getAfterParent() == null; - } - - /** - * Returns {@link NodeType#isAnnotation()} for this node's {@link DiffNode#nodeType}. - */ - public boolean isAnnotation() { - return this.nodeType.isAnnotation(); + return getParent(BEFORE) == null && getParent(AFTER) == null; } /** @@ -977,15 +587,19 @@ public boolean isAnnotation() { */ public int getID() { // Add one to ensure invalid (negative) line numbers don't cause issues. - int lineNumber = 1 + from.inDiff(); - Assert.assertTrue((lineNumber << 2*ID_OFFSET) >> 2*ID_OFFSET == lineNumber); + final int lineNumber = 1 + from.inDiff(); + + final int usedBitCount = DiffType.getRequiredBitCount() + NodeType.getRequiredBitCount(); + Assert.assertTrue((lineNumber << usedBitCount) >> usedBitCount == lineNumber); int id; id = lineNumber; - id <<= ID_OFFSET; - id += diffType.ordinal(); - id <<= ID_OFFSET; - id += nodeType.ordinal(); + + id <<= DiffType.getRequiredBitCount(); + id |= diffType.ordinal(); + + id <<= NodeType.getRequiredBitCount(); + id |= nodeType.ordinal(); return id; } @@ -997,12 +611,16 @@ public int getID() { * @param label The label the node should have. * @return The reconstructed DiffNode. */ - public static DiffNode fromID(final int id, String label) { - final int lowestBitsMask = (1 << ID_OFFSET) - 1; + public static DiffNode fromID(int id, String label) { + final int nodeTypeBitmask = (1 << NodeType.getRequiredBitCount()) - 1; + final int nodeTypeOrdinal = id & nodeTypeBitmask; + id >>= NodeType.getRequiredBitCount(); - final int nodeTypeOrdinal = id & lowestBitsMask; - final int diffTypeOrdinal = (id >> ID_OFFSET) & lowestBitsMask; - final int fromInDiff = (id >> (2*ID_OFFSET)) - 1; + final int diffTypeBitmask = (1 << DiffType.getRequiredBitCount()) - 1; + final int diffTypeOrdinal = id & diffTypeBitmask; + id >>= DiffType.getRequiredBitCount(); + + final int fromInDiff = id - 1; var nodeType = NodeType.values()[nodeTypeOrdinal]; return new DiffNode( @@ -1026,34 +644,32 @@ public void assertConsistency() { // check consistency of children lists and edges for (final DiffNode c : childOrder) { Assert.assertTrue(isChild(c), () -> "Child " + c + " of " + this + " is neither a before nor an after child!"); - if (c.getBeforeParent() != null) { - Assert.assertTrue(c.getBeforeParent().isBeforeChild(c), () -> "The beforeParent of " + c + " doesn't contain that node as child"); - } - if (c.getAfterParent() != null) { - Assert.assertTrue(c.getAfterParent().isAfterChild(c), () -> "The afterParent of " + c + " doesn't contain that node as child"); - } + Time.forAll(time -> { + if (c.getParent(time) != null) { + Assert.assertTrue(c.getParent(time).isChild(c, time), () -> "The parent " + time.toString().toLowerCase() + " the edit of " + c + " doesn't contain that node as child"); + } + }); } // a node with exactly one parent was edited - if (beforeParent == null && afterParent != null) { + if (getParent(BEFORE) == null && getParent(AFTER) != null) { Assert.assertTrue(isAdd()); } - if (beforeParent != null && afterParent == null) { + if (getParent(BEFORE) != null && getParent(AFTER) == null) { Assert.assertTrue(isRem()); } // a node with exactly two parents was not edited - if (beforeParent != null && afterParent != null) { + if (getParent(BEFORE) != null && getParent(AFTER) != null) { Assert.assertTrue(isNon()); } // Else and Elif nodes have an If or Elif as parent. if (this.isElse() || this.isElif()) { - if (beforeParent != null) { - Assert.assertTrue(beforeParent.isIf() || beforeParent.isElif(), "Before parent " + beforeParent + " of " + this + " is neither IF nor ELIF!"); - } - if (afterParent != null) { - Assert.assertTrue(afterParent.isIf() || afterParent.isElif(), "After parent " + afterParent + " of " + this + " is neither IF nor ELIF!"); - } + Time.forAll(time -> { + if (getParent(time) != null) { + Assert.assertTrue(getParent(time).isIf() || getParent(time).isElif(), time + " parent " + getParent(time) + " of " + this + " is neither IF nor ELIF!"); + } + }); } // Only if and elif nodes have a formula @@ -1112,6 +728,51 @@ public String toTextDiff() { return diff.toString(); } + /** + * Returns a view of this {@code DiffNode} as a variation node at the time {@code time}. + * + *

See the {@code project} function in section 3.1 of + * + * our paper. + */ + public Projection projection(Time time) { + Assert.assertTrue(getDiffType().existsAtTime(time)); + + if (projections[time.ordinal()] == null) { + projections[time.ordinal()] = new Projection(this, time); + } + + return projections[time.ordinal()]; + } + + /** + * Transforms a {@code VariationNode} into a {@code DiffNode} by diffing {@code variationNode} + * to itself. + * + * This is the inverse of {@link projection} iff the original {@link DiffNode} wasn't modified + * (all node had a {@link getDiffType diff type} of {@link DiffType#NON}). + */ + public static > DiffNode unchanged(VariationNode variationNode) { + int from = variationNode.getLineRange().getFromInclusive(); + int to = variationNode.getLineRange().getToExclusive(); + + var diffNode = new DiffNode( + DiffType.NON, + variationNode.getNodeType(), + new DiffLineNumber(from, from, from), + new DiffLineNumber(to, to, to), + variationNode.getDirectFeatureMapping(), + variationNode.getLabelLines() + ); + + for (var variationChildNode : variationNode.getChildren()) { + var diffChildNode = unchanged(variationChildNode); + Time.forAll(time -> diffNode.addChild(diffChildNode, time)); + } + + return diffNode; + } + @Override public String toString() { String s; diff --git a/src/main/java/org/variantsync/diffdetective/diff/difftree/DiffTree.java b/src/main/java/org/variantsync/diffdetective/diff/difftree/DiffTree.java index ed27cc7cd..d9e0b47b9 100644 --- a/src/main/java/org/variantsync/diffdetective/diff/difftree/DiffTree.java +++ b/src/main/java/org/variantsync/diffdetective/diff/difftree/DiffTree.java @@ -31,6 +31,8 @@ import java.util.function.Consumer; import java.util.function.Predicate; +import static org.variantsync.diffdetective.diff.difftree.Time.AFTER; +import static org.variantsync.diffdetective.diff.difftree.Time.BEFORE; import static org.variantsync.functjonal.Functjonal.when; /** @@ -338,17 +340,13 @@ public boolean isEmpty() { public void removeNode(DiffNode node) { Assert.assertTrue(node != root); - final DiffNode beforeParent = node.getBeforeParent(); - if (beforeParent != null) { - beforeParent.removeBeforeChild(node); - beforeParent.addBeforeChildren(node.removeBeforeChildren()); - } - - final DiffNode afterParent = node.getAfterParent(); - if (afterParent != null) { - afterParent.removeAfterChild(node); - afterParent.addAfterChildren(node.removeAfterChildren()); - } + Time.forAll(time -> { + final DiffNode parent = node.getParent(time); + if (parent != null) { + parent.removeChild(node, time); + parent.addChildren(node.removeChildren(time), time); + } + }); } /** @@ -394,8 +392,8 @@ public boolean test(final DiffNode d) { // The stranger is now known. cache.putIfAbsent(id, VisitStatus.VISITED); - final DiffNode b = d.getBeforeParent(); - final DiffNode a = d.getAfterParent(); + final DiffNode b = d.getParent(BEFORE); + final DiffNode a = d.getParent(AFTER); if (a == null && b == null) { // We found a second root node which is invalid. yield false; diff --git a/src/main/java/org/variantsync/diffdetective/diff/difftree/DiffType.java b/src/main/java/org/variantsync/diffdetective/diff/difftree/DiffType.java index 54f8dba7c..3d9f83271 100644 --- a/src/main/java/org/variantsync/diffdetective/diff/difftree/DiffType.java +++ b/src/main/java/org/variantsync/diffdetective/diff/difftree/DiffType.java @@ -142,4 +142,11 @@ public boolean existsAfter() { public boolean existsAtTime(Time time) { return (time == Time.BEFORE && this != ADD) || (time == Time.AFTER && this != REM); } + + /** + * Returns the number of bits required for storing {@link ordinal}. + */ + public static int getRequiredBitCount() { + return 3; + } } diff --git a/src/main/java/org/variantsync/diffdetective/diff/difftree/NodeType.java b/src/main/java/org/variantsync/diffdetective/diff/difftree/NodeType.java index 66a6feb85..9715a07e7 100644 --- a/src/main/java/org/variantsync/diffdetective/diff/difftree/NodeType.java +++ b/src/main/java/org/variantsync/diffdetective/diff/difftree/NodeType.java @@ -47,4 +47,11 @@ public static NodeType fromName(final String name) { throw new IllegalArgumentException("Given string \"" + name + "\" is not the name of a NodeType."); } + + /** + * Returns the number of bits required for storing {@link ordinal}. + */ + public static int getRequiredBitCount() { + return 3; + } } diff --git a/src/main/java/org/variantsync/diffdetective/diff/difftree/Projection.java b/src/main/java/org/variantsync/diffdetective/diff/difftree/Projection.java new file mode 100644 index 000000000..e372c973b --- /dev/null +++ b/src/main/java/org/variantsync/diffdetective/diff/difftree/Projection.java @@ -0,0 +1,168 @@ +package org.variantsync.diffdetective.diff.difftree; + +import java.util.List; +import java.util.Optional; + +import org.prop4j.Node; +import org.variantsync.diffdetective.diff.Lines; +import org.variantsync.diffdetective.variationtree.VariationNode; +import org.variantsync.functjonal.list.FilteredMappedListView; + +/** + * A view of a {@link DiffNode} as variation node at a specific time. + * + *

See the {@code project} function in section 3.1 of + * + * our paper. + * + *

This class has to be instantiated using {@link DiffNode#projection}. + * + *

Implementation note: It's ensured that identity can be checked using {@code ==}. This + * prevents unexpected behaviour if some code uses {@code ==} instead of {@link isSameAs} as + * documented in {@link VariationNode}. Although this is currently guaranteed by all + * implementations of {@link VariationNode} it should still be considered a bug if {@code ==} is + * used to check for identity ({@code null} checks are still allowed). + * + * @see DiffNode#projection + */ +public class Projection extends VariationNode { + private DiffNode backingNode; + private Time time; + + /** + * Creates a new projection of a {@link DiffNode}. + * Only {@link DiffNode} is allowed to call this method to guarantee the identity of this class + * (see above for details). If you want to get the projection of a {@link DiffNode} use + * {@link DiffNode#projection}. + * + * @param backingNode the {@link DiffNode} which should be projected + * @param time which projection this should be + */ + Projection(DiffNode backingNode, Time time) { + this.backingNode = backingNode; + this.time = time; + } + + public Time getTime() { + return time; + } + + public DiffNode getBackingNode() { + return backingNode; + } + + @Override + public Projection upCast() { + return this; + } + + + @Override + public NodeType getNodeType() { + return getBackingNode().nodeType; + } + + @Override + public List getLabelLines() { + return getBackingNode().getLabelLines(); + } + + @Override + public Lines getLineRange() { + return getBackingNode().getLinesAtTime(time); + } + + @Override + public void setLineRange(Lines lineRange) { + getBackingNode().setLinesAtTime(lineRange, time); + } + + @Override + public Projection getParent() { + var parent = getBackingNode().getParent(time); + + if (parent == null) { + return null; + } else { + return parent.projection(time); + } + } + + + @Override + public List getChildren() { + return FilteredMappedListView.filterMap( + getBackingNode().getChildOrder(), + (child) -> { + if (getBackingNode().isChild(child, time)) { + return Optional.of(child.projection(time)); + } else { + return Optional.empty(); + } + } + ); + } + + @Override + public void addChild(final Projection child) { + getBackingNode().addChild(child.getBackingNode(), time); + } + + @Override + public void insertChild(final Projection child, int index) { + // The method `DiffNode.addChild` can't be used here because `index` has a different + // meaning: For `DiffNode.addChild` it counts all children, before and after, but here + // it only counts children at `time`. + + var iterator = getBackingNode().getChildOrder().listIterator(); + for (int i = 0; i < index; ) { + if (!iterator.hasNext()) { + throw new IllegalArgumentException(); + } + + if (iterator.next().getDiffType().existsAtTime(time)) { + ++i; + } + } + + getBackingNode().insertChild(child.getBackingNode(), iterator.nextIndex(), time); + } + + @Override + public boolean removeChild(final Projection child) { + return getBackingNode().removeChild(child.getBackingNode(), time); + } + + @Override + public void removeAllChildren() { + getBackingNode().removeChildren(time); + } + + @Override + public Node getDirectFeatureMapping() { + return getBackingNode().getDirectFeatureMapping(); + } + + @Override + public int getID() { + return getBackingNode().getID(); + } + + @Override + public boolean isSameAs(Projection other) { + if (other != null && getClass() == other.getClass()) { + Projection otherProjection = (Projection) other; + return time.equals(otherProjection.time) && getBackingNode() == otherProjection.getBackingNode(); + } else { + return false; + } + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Projection projection = (Projection) o; + return time.equals(projection.time) && getBackingNode().equals(projection.getBackingNode()); + } +}; diff --git a/src/main/java/org/variantsync/diffdetective/diff/difftree/Time.java b/src/main/java/org/variantsync/diffdetective/diff/difftree/Time.java index 98306052e..21a18f1b9 100644 --- a/src/main/java/org/variantsync/diffdetective/diff/difftree/Time.java +++ b/src/main/java/org/variantsync/diffdetective/diff/difftree/Time.java @@ -15,7 +15,7 @@ public enum Time { * Invoke the given function for each time value (i.e., each value in this enum). * @param f callback */ - public static void forall(final Consumer

Provides common methods for querying variation trees and changing their structure. This class + * doesn't provide mutation methods for attributes which may be shared between different underlying + * nodes (for example a {@link Projection projection} of a {@link DiffNode}). Most prominently, + * there are no methods to change the {@link getNodeType type} or the {@link getLabelLines label} + * of this node. + * + *

There are many methods which are not abstract. These are convenience methods or algorithms + * acting on variation nodes where a separate class may be undesirable (for example because they are + * quite common or because the calling syntax {@code node.algorithm()} makes more sense than the + * alternative {@code Algorithm.run(node)}). + * + *

Identity of {@code VariationNode}s shouldn't be tested using {@code ==} because this might be + * a view which is instantiated multiple times for the same node (e.g., it might be that + * {@code getParent() != getParent()}). Instead the method {@link isSameAs} should be used to test + * for identity. + * + * @param the derived type (the type extending this class) + * + * @see assertConsistency + * @author Benjamin Moosherr + */ +public abstract class VariationNode> implements HasNodeType { + /** + * Returns this instance as the derived class type {@code T}. + * The deriving class will only have to return {@code this} here but this can't be implemented + * in the base class. If some derived class can't implement this method by returning + * {@code this}, it probably violates the requirements for the type parameter {@code T} (namely + * that it' the derived class itself). + */ + public abstract T upCast(); + + /** + * Returns this instance as a {@link VariationNode}. + * This is only useful for accessing private members inside of {@link VariationNode}. These + * can't be accessed if the type of the variable of this instance is {@code T} so a down cast is + * required. This function only exists to document this necessity and make it more readable. + */ + public VariationNode downCast() { + return this; + } + + /** + * Returns the node type of this node which determines the type of the represented element in + * the variation tree (e.g., mapping or artifact). + * + * @see HasNodeType + */ + public abstract NodeType getNodeType(); + + /** + * Returns the label of this node as an unmodifiable list of lines. + * + *

If {@link isArtifact} is {@code true} this may represent the source code of this artifact. + * Otherwise it may represent the preprocessor expression which was parsed to obtain + * {@link getDirectFeatureMapping}. In either case, this label may be an arbitrary value, + * selected according to the needs of the user of this class. + */ + public abstract List getLabelLines(); + + /** + * Returns the range of line numbers of this node's corresponding source code. + * + * @see setLineRange + */ + public abstract Lines getLineRange(); + + /** + * Sets the range of line numbers of this node's corresponding source code. + * + * @see getLineRange + */ + public abstract void setLineRange(Lines lineRange); + + /** + * Returns the parent of this node, or {@code null} if this node doesn't have a parent. + * + * @see drop + * @see addBelow + */ + public abstract T getParent(); + + /** + * Returns an unmodifiable list representing the children of this node. + * + *

The following invariant has to hold for all {@code node}s: + * + * for (var child : node.getChildren()) { + * Assert.assertTrue(node.isSameAs(child.getParent(node))) + * } + * + * + * @see isChild + * @see addChild + * @see removeChild + */ + public abstract List getChildren(); + + /** + * Returns {@code true} iff this node has no parent. + * + * @see getParent + */ + public boolean isRoot() { + return getParent() == null; + } + + /** + * Returns {@code true} iff this node has no children. + * + * @see getChildren + */ + public boolean isLeaf() { + return getChildren().isEmpty(); + } + + /** + * Returns the number of child nodes. + * + *

Note: This is O(n) for {@link Projection}. + * + * @see getChildCount + */ + public int getChildCount() { + return getChildren().size(); + } + + /** + * Computes the length of the path from the root node to this node. + */ + public int getDepth() { + if (isRoot()) { + return 0; + } + + return getParent().getDepth() + 1; + } + + /** + * Returns the first {@code if} node in the path from this node upwards to the root. + */ + public T getIfNode() { + if (isIf()) { + return this.upCast(); + } + return getParent().getIfNode(); + } + + /** + * Returns {@code true} iff this node is the parent of the given node. + * + * @see getChildren + */ + public boolean isChild(T child) { + return child.getParent().isSameAs(this.upCast()); + } + + /** + * Returns the index of the given child in the list of children of this node. + * Returns -1 if the given node is not a child of this node. + * + *

Warning: If this is a {@link Projection}, then the returned index may be different to the + * index returned by {@link DiffNode#indexOfChild}. + * + * @see getChildren + */ + public int indexOfChild(final T child) { + return getChildren().indexOf(child); + } + + /** + * The same as {@link insertChild} but puts the node at the end of the children list instead of + * inserting it at a specific index. + * + * @throws IllegalArgumentException if {@code child} already has a parent + * @see getChildren + */ + public abstract void addChild(final T child); + + /** + * Adds a child before the given index to the children list of this node and sets its parent to + * this node. + * + *

When calling {@link indexOfChild} with {@code child} the returned index will be + * {@code index} as long as the children list isn't modified. + * + * @throws IllegalArgumentException if {@code child} already has a parent + * @see addChildren + */ + public abstract void insertChild(final T child, int index); + + /** + * Adds the given nodes in traversal order as children using + * {@link addChild}. + * * + * @throws IllegalArgumentException if any child of {@code children} already has a parent + */ + public void addChildren(final Collection children) { + for (final var child : children) { + addChild(child); + } + } + + /** + * Removes the given node from this node's children list and sets the parent of {@code child} + * to {@code null}. + * + * @return {@code true} iff the child was removed, {@code false} iff {@code child} is not a + * child of this node + * @throws IllegalArgumentException if {@code childe} is not a child of this node + * @see removeChildren + * @see getChildren + */ + public abstract boolean removeChild(final T child); + + /** + * Removes the given nodes from the children list using {@link removeChild}. + + * @throws IllegalArgumentException if any child in {@code childrenToRemove} is not a child of + * this node + * @see removeAllChildren + */ + public void removeChildren(final Collection childrenToRemove) { + for (final var childToRemove : childrenToRemove) { + removeChild(childToRemove); + } + } + + /** + * Removes all children of this node and sets their parent to {@code null}. + * Afterwards, this node will have no children. + */ + public abstract void removeAllChildren(); + + /** + * Adds this subtree below the given parent. + * Inverse of {@link drop}. + * + * @see addChild + * @throws IllegalArgumentException if this node already has a parent + */ + public void addBelow(final T parent) { + if (parent != null) { + parent.addChild(this.upCast()); + } + } + + /** + * Removes this subtree from its parents by removing it as child from its parent and settings + * the parent of this node to {@code null}. + * Inverse of {@link addBelow}. + * + * @see removeChild + */ + public void drop() { + if (getParent() != null) { + getParent().removeChild(this.upCast()); + } + } + + /** + * Removes all children from the given node and adds them as children to this node. + * The order of the children is preserved. The given node will have no children afterwards. + * + * @param other The node whose children should be stolen. + * @see addChildren + * @see removeAllChildren + */ + public void stealChildrenOf(final T other) { + addChildren(other.getChildren()); + other.removeAllChildren(); + } + + /** + * Returns the formula that is stored in this node. + * The formula is not {@code null} for + * {@link NodeType#isConditionalAnnotation mapping nodes with annotations} and {@code null} + * otherwise ({@link NodeType#ARTIFACT}, {@link NodeType#ELSE}). + * + *

If the type parameter {@code T} of this class is not a concrete variation tree, then the + * returned {@link Node formula} should be treated as unmodifiable to prevent undesired side + * effects (e.g., to {@link DiffNode}s). + */ + public abstract Node getDirectFeatureMapping(); + + /** + * Same as {@link getFeatureMapping} but returns a list of formulas representing a conjunction. + */ + private List getFeatureMappingClauses() { + final var parent = getParent(); + + if (isElse() || isElif()) { + List and = new ArrayList<>(); + + if (isElif()) { + and.add(getDirectFeatureMapping()); + } + + // Negate all previous cases + var ancestor = parent; + while (!ancestor.isIf()) { + if (ancestor.isElif()) { + and.add(negate(ancestor.getDirectFeatureMapping())); + } else { + throw new RuntimeException("Expected If or Elif above Else or Elif but got " + ancestor.getNodeType() + " from " + ancestor); + // Assert.assertTrue(ancestor.isArtifact()); + } + ancestor = ancestor.getParent(); + } + and.add(negate(ancestor.getDirectFeatureMapping())); + + return and; + } else if (isArtifact()) { + return parent.downCast().getFeatureMappingClauses(); + } + + return List.of(getDirectFeatureMapping()); + } + + /** + * Returns the full feature mapping formula of this node. + * + *

The feature mapping of an {@link NodeType#IF} node is its {@link getDirectFeatureMapping + * direct feature mapping}. The feature mapping of {@link NodeType#ELSE} and {@link + * NodeType#ELIF} nodes is determined by all formulas in the respective if-elif-else chain. The + * feature mapping of an {@link NodeType#ARTIFACT artifact} node is the feature mapping of its + * parent. See Equation (1) in + * + * our paper. + * + * @return the feature mapping of this node + */ + public Node getFeatureMapping() { + final List fmClauses = getFeatureMappingClauses(); + if (fmClauses.size() == 1) { + return fmClauses.get(0); + } + return new And(fmClauses); + } + + /** + * Returns the presence condition clauses of this node. + * + * @return a list representing a conjunction (i.e., all clauses should be combined with boolean + * AND) + * @see getPresenceCondition + */ + private List getPresenceConditionClauses() { + final var parent = getParent(); + + if (isElse() || isElif()) { + final List clauses = new ArrayList<>(getFeatureMappingClauses()); + + // Find corresponding if + var correspondingIf = parent; + while (!correspondingIf.isIf()) { + correspondingIf = correspondingIf.getParent(); + } + + // If this elif-else-chain was again nested in another annotation, add its pc. + final var outerNesting = correspondingIf.getParent(); + if (outerNesting != null) { + clauses.addAll(outerNesting.downCast().getPresenceConditionClauses()); + } + + return clauses; + } else if (isArtifact()) { + return parent.downCast().getPresenceConditionClauses(); + } + + // this is mapping or root + final List clauses; + if (parent == null) { + clauses = new ArrayList<>(1); + } else { + clauses = parent.downCast().getPresenceConditionClauses(); + } + clauses.add(getDirectFeatureMapping()); + return clauses; + } + + /** + * Returns the presence condition of this node. + * See Equation (2) in + * + * our paper. + */ + public Node getPresenceCondition() { + final List pcClauses = getPresenceConditionClauses(); + if (pcClauses.size() == 1) { + return pcClauses.get(0); + } + return new And(pcClauses); + } + + /** + * Traverses all nodes in this subtree in preorder. + */ + public void forAllPreorder(Consumer action) { + action.accept(this.upCast()); + + for (var child : getChildren()) { + child.forAllPreorder(action); + } + } + + /** + * Returns a copy of this variation tree in a {@link VariationTreeNode concrete variation tree implementation}. + * If the type parameter {@code T} of this class is {@link VariationTreeNode} then this is + * effectively a deep copy. + */ + public VariationTreeNode toVariationTree() { + // Copy mutable attributes to allow modifications of the new node. + var newNode = new VariationTreeNode( + getNodeType(), + getDirectFeatureMapping().clone(), + getLineRange(), + new ArrayList(getLabelLines()) + ); + + for (var child : getChildren()) { + newNode.addChild(child.toVariationTree()); + } + + return newNode; + } + + /** + * Checks that this node satisfies some easy to check invariants. + * In particular, this method checks that + *

    + *
  • if-chains are nested correctly, + *
  • the root is an {@link NodeType#IF} with the feature mapping {@code "true"}, + *
  • the feature mapping is {@code null} iff {@code isConditionalAnnotation} is {@code false} + * and + *
  • all edges are well-formed (e.g., edges can be inconsistent because edges are + * double-linked). + *
+ * + *

Some invariants are not checked. These include + *

    + *
  • There should be no cycles and + *
  • {@link getID} should be unique in the whole variation tree. + *
+ * + * @see Assert#assertTrue + * @throws AssertionError when an inconsistency is detected. + */ + public void assertConsistency() { + // ELSE and ELIF nodes have an IF or ELIF as parent. + if (isElse() || isElif()) { + Assert.assertTrue( + getParent().isIf() || getParent().isElif(), + "Parent " + getParent() + " of " + this + " is neither IF nor ELIF"); + } + + // Presence/absence of the direct feature mapping + if (isConditionalAnnotation()) { + Assert.assertTrue( + getDirectFeatureMapping() != null, + "The conditional annotation " + this + " doesn't have a direct feature mapping"); + } else { + Assert.assertTrue( + getDirectFeatureMapping() == null, + "The node " + this + " shouldn't have a direct feature mapping"); + } + + // The root has to be an IF + if (isRoot()) { + Assert.assertTrue( + isIf(), + "The root has to be an IF"); + + Assert.assertTrue( + getDirectFeatureMapping().equals(FixTrueFalse.True), + "The root has to have the feature mapping 'true'"); + } + + // check consistency of children lists and edges + for (var child : getChildren()) { + Assert.assertTrue( + child.getParent().isSameAs(this.upCast()), () -> + "The parent (" + this + ") of " + child + " is not set correctly"); + } + } + + + + /** + * Returns an integer that uniquely identifies this node within its tree. + * + *

Some attributes may be recovered from this ID but this depends on the derived class. For + * example {@link VariationTreeNode#fromID} can recover {@link getNodeType} and + * {@link getLineRange the start line number}. Beware that {@link Projection} returns + * {@link DiffNode#getID} so this id is not fully compatible with + * {@link VariationTreeNode#getID}. + */ + public abstract int getID(); + + /** + * Checks if {@code other} represents the same node as this node. + * + *

The difference between this method and {@link equals} is the same as the difference + * between {@code ==} and {@link equals}: {@code isSameAs} respects the identity of the backing + * node and {@link equals} does not. + * + *

This method has to be used instead of {@code ==} because multiple instances of this view + * representing the same node might be created (i.e., {@code getParent() == getParent()} might + * not always hold). + */ + public abstract boolean isSameAs(T other); + + /** + * Unparses the labels of this subtree into {@code output}. + * + *

This method assumes that all labels of this subtree represent source code lines. + * + * @throws IOException iff output throws an {@link IOException} + */ + public void printSourceCode(BufferedWriter output) throws IOException { + for (final var line : getLabelLines()) { + output.write(line); + output.newLine(); + } + + for (final var child : getChildren()) { + child.printSourceCode(output); + } + + // Add #endif after macro + if (isIf() && !isRoot()) { + output.write("#endif"); + output.newLine(); + } + } +} diff --git a/src/main/java/org/variantsync/diffdetective/variationtree/VariationTree.java b/src/main/java/org/variantsync/diffdetective/variationtree/VariationTree.java new file mode 100644 index 000000000..3ae95b657 --- /dev/null +++ b/src/main/java/org/variantsync/diffdetective/variationtree/VariationTree.java @@ -0,0 +1,122 @@ +package org.variantsync.diffdetective.variationtree; + +import org.variantsync.diffdetective.diff.difftree.NodeType; // For Javadoc +import org.variantsync.diffdetective.diff.difftree.parse.DiffTreeParser; +import org.variantsync.diffdetective.diff.result.DiffParseException; +import org.variantsync.diffdetective.feature.CPPAnnotationParser; +import org.variantsync.diffdetective.util.Assert; +import org.variantsync.diffdetective.variationtree.source.LocalFileSource; +import org.variantsync.diffdetective.variationtree.source.VariationTreeSource; + +import java.io.BufferedReader; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +import static org.variantsync.diffdetective.diff.difftree.Time.BEFORE; + +/** + * Representation of a concrete variation tree with source information. + * + * @param root the root of the variation tree + * @param source from which source code the variation tree was obtained + * @see VariationTreeNode + * @author Benjamin Moosherr + */ +public record VariationTree( + VariationTreeNode root, + VariationTreeSource source +) { + /** Creates a {@code VariationTree} with the given root and an unknown source. */ + public VariationTree(VariationTreeNode root) { + this(root, VariationTreeSource.Unknown); + } + + /** Creates a {@code VariationTree} with the given root and source. */ + public VariationTree(VariationTreeNode root, VariationTreeSource source) { + this.root = root; + this.source = source; + + Assert.assertTrue(root.isRoot()); + } + + /** + * Same as {@link fromFile(Path, boolean, CPPAnnotationParser)} but with + * a {@link CPPAnnotationParser#Default default annotation parser}. + */ + public static VariationTree fromFile( + final Path p, + final boolean collapseMultipleCodeLines + ) throws IOException, DiffParseException { + return fromFile(p, collapseMultipleCodeLines, CPPAnnotationParser.Default); + } + + /** + * Same as {@link fromFile(BufferedReader, VariationTreeSource, boolean, CPPAnnotationParser)} + * but registers {@code path} as source. + */ + public static VariationTree fromFile( + final Path path, + final boolean collapseMultipleCodeLines, + final CPPAnnotationParser annotationParser + ) throws IOException, DiffParseException { + try (BufferedReader file = Files.newBufferedReader(path)) { + return fromFile( + file, + new LocalFileSource(path), + collapseMultipleCodeLines, + annotationParser + ); + } + } + + /** + * Same as {@link fromFile(BufferedReader, VariationTreeSource, boolean, CPPAnnotationParser)} + * but with a {@link CPPAnnotationParser#Default default annotation parser}. + */ + public static VariationTree fromFile( + final BufferedReader input, + final VariationTreeSource source, + final boolean collapseMultipleCodeLines + ) throws IOException, DiffParseException { + return fromFile( + input, + source, + collapseMultipleCodeLines, + CPPAnnotationParser.Default + ); + } + + /** + * Parses a {@code VariationTree} from source code with C preprocessor annotations. + * + * @param input the source code to be parsed + * @param collapseMultipleCodeLines Set to true if subsequent lines of source code with the same + * {@link NodeType type} should be collapsed into a single artifact node representing all lines + * at once. + * @param annotationParser the parser that is used to parse preprocessor expressions + * @return a new {@code VariationTree} representing {@code input} + * @throws IOException if {@code input} throws {@code IOException} + * @throws DiffParseException if some preprocessor annotations can't be parsed + */ + public static VariationTree fromFile( + final BufferedReader input, + final VariationTreeSource source, + final boolean collapseMultipleCodeLines, + final CPPAnnotationParser annotationParser + ) throws IOException, DiffParseException { + VariationTreeNode tree = DiffTreeParser + .createVariationTree(input, collapseMultipleCodeLines, false, annotationParser) + .getRoot() + // Arbitrarily choose the BEFORE projection as both should be equal. + .projection(BEFORE) + .toVariationTree(); + + return new VariationTree(tree, source); + } + + @Override + public String toString() { + return "variation tree from " + source; + } +} diff --git a/src/main/java/org/variantsync/diffdetective/variationtree/VariationTreeNode.java b/src/main/java/org/variantsync/diffdetective/variationtree/VariationTreeNode.java new file mode 100644 index 000000000..88ed97f6c --- /dev/null +++ b/src/main/java/org/variantsync/diffdetective/variationtree/VariationTreeNode.java @@ -0,0 +1,350 @@ +package org.variantsync.diffdetective.variationtree; + +import org.prop4j.Node; +import org.variantsync.diffdetective.diff.DiffLineNumber; +import org.variantsync.diffdetective.diff.Lines; +import org.variantsync.diffdetective.diff.difftree.DiffNode; // For Javdoc +import org.variantsync.diffdetective.diff.difftree.DiffTree; // For Javdoc +import org.variantsync.diffdetective.diff.difftree.DiffType; +import org.variantsync.diffdetective.diff.difftree.NodeType; +import org.variantsync.diffdetective.util.Assert; +import org.variantsync.diffdetective.util.fide.FixTrueFalse; + +import java.util.*; + +/** + * A single node in a variation tree. + * + *

A variation tree is a representation of source code in a software product line. Each node in + * such a tree is either an {@link isArtifact artifact} or a {@link isAnnotation mapping node}. + * Artifacts represent source code which may be reused in multiple variants. In contrast, mappings + * do not contain source code of a specific variant. They store information about which variants + * contain the artifacts it has as {@link getChildren children} in the form of a + * {@link getFeatureMapping feature mapping}. + * + *

See definition 2.2 of + * + * our ESEC/FSE'22 paper for a mathematical formalization or variation trees. In contrast to + * this formalization, this implementation optimizes for variation trees obtained from source code + * annotated with the C preprocessor syntax. This mostly means that there is the additional + * {@link NodeType#ELIF} node type. + * + *

This class contains references to all of its children and its parent so all connected nodes of + * a variation tree can be reached through each node of the variation tree. Nevertheless, most of + * the time only node itself or its subtree (the reflexive hull of {@link getChildren}) is meant + * when when referencing a {@code VariationTreeNode}. Use {@link VariationTree} to unambiguously + * refer to a whole variation tree. + * + *

If possible, algorithms should be using {@link VariationNode} instead of this concrete + * implementation. This allows the usage of a projection of a {@link DiffNode} instead of a concrete + * variation node. + * + *

To compare two variation trees use {@link DiffTree} which uses the aforementioned + * {@link DiffNode}s. + * + * @see VariationTree + * @see DiffTree + * @see DiffNode + * @author Benjamin Moosherr + */ +public class VariationTreeNode extends VariationNode { + /** + * The node type of this node, which determines the type of the represented + * element in the diff (e.g., mapping or artifact). + */ + private final NodeType nodeType; + + /** + * The range of line numbers of this node's corresponding source code. + */ + private Lines lineRange; + + /** + * A list of lines representing the label of this node. + */ + private List label; + + /** + * The direct feature mapping of this node. + * + *

This is {@code null} iff {@link isConditionalAnnotation} is {@code false}. + */ + private Node featureMapping; + + /** + * The parent of this node. It's {@code null} iff this node doesn't have a parent. + * + *

Invariant: Iff {@code parent != null} then {@code parent.childOrder.contains(this)}. + */ + private VariationTreeNode parent; + + /** + * The list for maintaining the order of all children of this node. + * + * @see parent + */ + private final List childOrder; + + /** + * Creates a new node of a variation tree. + * + * The newly created node is not connected to any other nodes. + * + * The new node takes ownership of the given label without copying it. Beware of any further + * modifications to this list. + * + * @param nodeType the type of this node + * @param featureMapping the direct feature mapping of this node, has be non null iff + * {@code nodeType.isConditionalAnnotation} is {@code true} + * @param lineRange the line range of the code of {@code label} + * @param label a list of lines used as label + * @see addChild + * @see addBelow + */ + public VariationTreeNode( + NodeType nodeType, + Node featureMapping, + Lines lineRange, + List label + ) { + super(); + + this.nodeType = nodeType; + this.lineRange = lineRange; + this.label = label; + this.featureMapping = featureMapping; + + this.childOrder = new ArrayList<>(); + } + + /** + * Convenience constructor for creating the root of a {@link VariationTree}. + * + *

The newly created root has no children yet. + * + *

The root is a neutral annotation (i.e., its type if {@link NodeType#IF} and its feature + * mapping is {@code "true"}). The label of this node is empty and therefore the line numbers + * are {@link DiffLineNumber#InvalidLineNumber invalid}. + * + * @see addChild + */ + public static VariationTreeNode createRoot() { + return new VariationTreeNode( + NodeType.IF, + FixTrueFalse.True, + Lines.Invalid(), + new ArrayList<>() + ); + } + + /** + * Convenience constructor for creating an artifact node of a {@link VariationTree}. + * + * @param lineRange the line range of the code of {@code label} + * @param label a list of lines used as label + * @see addBelow + */ + public static VariationTreeNode createArtifact(Lines lineRange, List label) { + return new VariationTreeNode(NodeType.ARTIFACT, null, lineRange, label); + } + + @Override + public VariationTreeNode upCast() { + return this; + } + + @Override + public NodeType getNodeType() { + return nodeType; + } + + @Override + public List getLabelLines() { + return Collections.unmodifiableList(label); + } + + /** + * Replaces the current label by {@code newLabelLines}. + * + *

The given list is not copied, so modifications of {@code newLabelLines} will be visible by + * {@link getLabelLines}. + * + * @see getLabelLines + */ + public void setLabelLines(List newLabelLines) { + label = newLabelLines; + } + + /** + * Adds the given lines to the source code lines of this node. + * + * @param lines lines to add + * @see getLabelLines + */ + public void addLines(final List lines) { + this.label.addAll(lines); + } + + @Override + public Lines getLineRange() { + return lineRange; + } + + @Override + public void setLineRange(Lines lineRange) { + this.lineRange = lineRange; + } + + @Override + public VariationTreeNode getParent() { + return parent; + } + + /** + * Sets {@code newParent} as the new parent of this node. + * + *

This node must not have a parent yet. + */ + private void setParent(final VariationTreeNode newParent) { + Assert.assertTrue(parent == null); + this.parent = newParent; + } + + @Override + public List getChildren() { + return Collections.unmodifiableList(childOrder); + } + + @Override + public void addChild(final VariationTreeNode child) { + child.setParent(this); + childOrder.add(child); + } + + @Override + public void insertChild(final VariationTreeNode child, int index) { + child.setParent(this); + childOrder.add(index, child); + } + + @Override + public boolean removeChild(final VariationTreeNode child) { + if (isChild(child)) { + child.parent = null; + childOrder.remove(child); + return true; + } + return false; + } + + @Override + public void removeAllChildren() { + for (var child : childOrder) { + child.parent = null; + } + + childOrder.clear(); + } + + @Override + public Node getDirectFeatureMapping() { + return featureMapping; + } + + /** + * Returns an integer that uniquely identifiers this node within its variation tree. + * + *

From the returned id a new node with all essential attributes ({@link getNodeType node + * type} and {@link getLineRange start line number}) can be reconstructed by using + * {@link fromID}. + * + *

Note that this encoding assumes that line numbers fit into {@code 26} bits. + */ + @Override + public int getID() { + // Add one to ensure invalid (negative) line numbers don't cause issues. + final int lineNumber = 1 + getLineRange().getFromInclusive(); + + final int usedBitCount = DiffType.getRequiredBitCount() + NodeType.getRequiredBitCount(); + Assert.assertTrue((lineNumber << usedBitCount) >> usedBitCount == lineNumber); + + int id; + id = lineNumber; + + // This makes `VariationTreeNode.toID` compatible with `DiffNode.toID` + id <<= DiffType.getRequiredBitCount(); + id |= DiffType.NON.ordinal(); + + id <<= NodeType.getRequiredBitCount(); + id |= nodeType.ordinal(); + return id; + } + + /** + * Reconstructs a node from the given {@link getID id} and label. + * The almost-inverse function is {@link getID()} but the conversion is not lossless. + * + * @param id the id from which to reconstruct the node + * @param label the label the node should have + * @return the reconstructed node + */ + public static VariationTreeNode fromID(int id, List label) { + final int nodeTypeBitmask = (1 << NodeType.getRequiredBitCount()) - 1; + final int nodeTypeOrdinal = id & nodeTypeBitmask; + id >>= NodeType.getRequiredBitCount(); + + final int diffTypeBitmask = (1 << DiffType.getRequiredBitCount()) - 1; + Assert.assertEquals(DiffType.NON.ordinal(), id & diffTypeBitmask); + id >>= DiffType.getRequiredBitCount(); + + final int from = id - 1; + + var nodeType = NodeType.values()[nodeTypeOrdinal]; + return new VariationTreeNode( + nodeType, + nodeType.isConditionalAnnotation() ? FixTrueFalse.True : null, + Lines.SingleLine(from), + label + ); + } + + @Override + public boolean isSameAs(VariationTreeNode other) { + return this == other; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + var other = (VariationTreeNode) o; + return nodeType == other.nodeType && lineRange.equals(other.lineRange) && Objects.equals(featureMapping, other.featureMapping) && label.equals(other.label); + } + + /** + * Compute a hash using all available attributes. + * + *

This implementation doesn't strictly adhere to the contract required by {@code Object}, + * because some attributes (for example the line numbers) can be changed during the lifetime of + * a node. So when using something like a {@code HashSet} the user of this class has to be + * careful with any modifications of attributes. + */ + @Override + public int hashCode() { + return Objects.hash(nodeType, lineRange, featureMapping, label); + } + + @Override + public String toString() { + String s; + if (isArtifact()) { + s = String.format("%s in the lines %d", nodeType, lineRange); + } else if (isRoot()) { + s = "ROOT"; + } else { + s = String.format("%s in the lines %d with \"%s\"", nodeType, + lineRange, featureMapping); + } + return s; + } +} diff --git a/src/main/java/org/variantsync/diffdetective/variationtree/source/GitSource.java b/src/main/java/org/variantsync/diffdetective/variationtree/source/GitSource.java new file mode 100644 index 000000000..a223f50eb --- /dev/null +++ b/src/main/java/org/variantsync/diffdetective/variationtree/source/GitSource.java @@ -0,0 +1,27 @@ +package org.variantsync.diffdetective.variationtree.source; + +import java.net.URL; +import java.nio.file.Path; + +/** + * A file at a specific commit in a Git repository. + * + *

The parameters of this record should be suitably chosen, so that the following commands can be + * executed in a shell to obtain the referenced source code: + * + * git clone "$repository" repository + * cd repository + * git switch -d "$commitHash" + * cat "$path" + * + */ +public record GitSource( + URL repository, + String commitHash, + Path path +) implements VariationTreeSource { + @Override + public String toString() { + return path.toString() + " at " + commitHash + " of " + repository; + } +} diff --git a/src/main/java/org/variantsync/diffdetective/variationtree/source/LocalFileSource.java b/src/main/java/org/variantsync/diffdetective/variationtree/source/LocalFileSource.java new file mode 100644 index 000000000..2e85d0573 --- /dev/null +++ b/src/main/java/org/variantsync/diffdetective/variationtree/source/LocalFileSource.java @@ -0,0 +1,13 @@ +package org.variantsync.diffdetective.variationtree.source; + +import java.nio.file.Path; + +/** + * A reference to a file with path {@code path} in the local file system. + */ +public record LocalFileSource(Path path) implements VariationTreeSource { + @Override + public String toString() { + return "file://" + path.toString(); + } +} diff --git a/src/main/java/org/variantsync/diffdetective/variationtree/source/VariationTreeSource.java b/src/main/java/org/variantsync/diffdetective/variationtree/source/VariationTreeSource.java new file mode 100644 index 000000000..963917ccf --- /dev/null +++ b/src/main/java/org/variantsync/diffdetective/variationtree/source/VariationTreeSource.java @@ -0,0 +1,19 @@ +package org.variantsync.diffdetective.variationtree.source; + +/** + * A reference to the source of the variation tree. + */ +public interface VariationTreeSource { + /** + * The source of the variation tree is unknown. + * Should be avoided if possible. + */ + public static VariationTreeSource Unknown = new VariationTreeSource() { + @Override + public String toString() { + return "unknown"; + } + }; + + String toString(); +} diff --git a/src/test/java/MarlinDebug.java b/src/test/java/MarlinDebug.java index 9357e7f78..b38218e94 100644 --- a/src/test/java/MarlinDebug.java +++ b/src/test/java/MarlinDebug.java @@ -31,6 +31,9 @@ import java.nio.file.Paths; import java.util.List; +import static org.variantsync.diffdetective.diff.difftree.Time.AFTER; +import static org.variantsync.diffdetective.diff.difftree.Time.BEFORE; + @Deprecated @Disabled public class MarlinDebug { @@ -110,8 +113,8 @@ public static void testCommit(final RepoInspection repoInspection, final String //LineGraphExportOptions.RenderError().accept(patch, e); Logger.error(e); Logger.info("Died at node {}", node.toString()); - Logger.info(" before parent: {}", node.getBeforeParent()); - Logger.info(" after parent: {}", node.getBeforeParent()); + Logger.info(" before parent: {}", node.getParent(BEFORE)); + Logger.info(" after parent: {}", node.getParent(AFTER)); Logger.info("isAdd: {}", node.isAdd()); Logger.info("isRem: {}", node.isRem()); Logger.info("isNon: {}", node.isNon()); diff --git a/src/test/java/PCTest.java b/src/test/java/PCTest.java index cb5c3d0b7..6969ed687 100644 --- a/src/test/java/PCTest.java +++ b/src/test/java/PCTest.java @@ -14,6 +14,8 @@ import java.util.Map; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.variantsync.diffdetective.diff.difftree.Time.AFTER; +import static org.variantsync.diffdetective.diff.difftree.Time.BEFORE; import static org.variantsync.diffdetective.util.fide.FormulaUtils.negate; public class PCTest { @@ -75,11 +77,11 @@ public void test(final TestCase testCase) throws IOException, DiffParseException final String text = node.getLabel().trim(); final ExpectedPC expectedPC = testCase.expectedResult.getOrDefault(text, null); if (expectedPC != null) { - Node pc = node.getBeforePresenceCondition(); + Node pc = node.getPresenceCondition(BEFORE); assertTrue( SAT.equivalent(pc, expectedPC.before), errorAt(text, "before", pc, expectedPC.before)); - pc = node.getAfterPresenceCondition(); + pc = node.getPresenceCondition(AFTER); assertTrue( SAT.equivalent(pc, expectedPC.after), errorAt(text, "after", pc, expectedPC.after)); diff --git a/src/test/java/StaticAsserts.java b/src/test/java/StaticAsserts.java new file mode 100644 index 000000000..9d7b6c1e1 --- /dev/null +++ b/src/test/java/StaticAsserts.java @@ -0,0 +1,23 @@ +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; +import org.variantsync.diffdetective.diff.difftree.DiffType; +import org.variantsync.diffdetective.diff.difftree.NodeType; + +public class StaticAsserts { + @Test + void testDiffTypeBitCount() { + assertTrue( + DiffType.values().length <= Math.pow(2, DiffType.getRequiredBitCount()), + "Using `DiffType.getRequiredBitCount()` bits is not enough to store all values of `DiffType`" + ); + } + + @Test + void testNodeTypeBitCount() { + assertTrue( + NodeType.values().length <= Math.pow(2, NodeType.getRequiredBitCount()), + "Using `NodeType.getRequiredBitCount()` bits is not enough to store all values of `NodeType`" + ); + } +} diff --git a/src/test/java/TestLineNumbers.java b/src/test/java/TestLineNumbers.java index 7bcb95552..3c4917271 100644 --- a/src/test/java/TestLineNumbers.java +++ b/src/test/java/TestLineNumbers.java @@ -14,6 +14,9 @@ import java.util.Map; import java.util.function.Function; +import static org.variantsync.diffdetective.diff.difftree.Time.AFTER; +import static org.variantsync.diffdetective.diff.difftree.Time.BEFORE; + public class TestLineNumbers { private static final Path resDir = Constants.RESOURCE_DIR.resolve("diffs/linenumbers"); private record TestCase(String filename, Map> expectedLineNumbers) { } @@ -69,9 +72,9 @@ private static void printLineNumbers(final DiffTree diffTree) { + " " + node.nodeType + " \"" + node.getLabel().trim() + " with ID " + node.getID() - + "\" old: " + node.getLinesBeforeEdit() + + "\" old: " + node.getLinesAtTime(BEFORE) + ", diff: " + node.getLinesInDiff() - + ", new: " + node.getLinesAfterEdit()) + + ", new: " + node.getLinesAtTime(AFTER)) ); System.out.println(); }