From 93397adecaf55e64d21979d89ba213cb008cee60 Mon Sep 17 00:00:00 2001
From: Benjamin Moosherr <benjamin.moosherr@uni-ulm.de>
Date: Sun, 30 Oct 2022 19:00:10 +0100
Subject: [PATCH 01/11] Generalize before and after methods using `Time`

---
 .../diffdetective/diff/DiffLineNumber.java    |  30 +-
 .../diff/difftree/DiffGraph.java              |   7 +-
 .../diffdetective/diff/difftree/DiffNode.java | 522 +++++-------------
 .../diffdetective/diff/difftree/DiffTree.java |  18 +-
 .../diffdetective/diff/difftree/Time.java     |  11 +
 .../diff/difftree/serialize/Format.java       |  11 +-
 .../serialize/edgeformat/EdgeLabelFormat.java |  13 +-
 .../CollapseNestedNonEditedAnnotations.java   |  17 +-
 .../transform/CutNonEditedSubtrees.java       |   7 +-
 .../NaiveMovedArtifactDetection.java          |   7 +-
 .../diff/difftree/transform/Starfold.java     |   2 +-
 .../editclass/proposed/AddToPC.java           |   4 +-
 .../editclass/proposed/AddWithMapping.java    |   4 +-
 .../editclass/proposed/Generalization.java    |   7 +-
 .../proposed/ProposedEditClasses.java         |  11 +-
 .../editclass/proposed/Reconfiguration.java   |   7 +-
 .../editclass/proposed/Refactoring.java       |   7 +-
 .../editclass/proposed/RemFromPC.java         |   4 +-
 .../editclass/proposed/RemWithMapping.java    |   4 +-
 .../editclass/proposed/Specialization.java    |   7 +-
 .../editclass/proposed/Untouched.java         |   7 +-
 .../mining/RunningExampleFinder.java          |   6 +-
 .../elementary/FeatureContextOfAddToPC.java   |   4 +-
 .../FeatureContextOfAddWithMapping.java       |   4 +-
 .../elementary/FeatureContextOfRemFromPC.java |   4 +-
 .../FeatureContextOfRemWithMapping.java       |   4 +-
 .../pattern/semantic/AddIfdefElif.java        |   6 +-
 .../pattern/semantic/AddIfdefElse.java        |   4 +-
 .../pattern/semantic/AddIfdefWrapElse.java    |   4 +-
 .../pattern/semantic/AddIfdefWrapThen.java    |   4 +-
 .../pattern/semantic/MoveElse.java            |   8 +-
 src/test/java/MarlinDebug.java                |   7 +-
 src/test/java/PCTest.java                     |   6 +-
 src/test/java/TestLineNumbers.java            |   7 +-
 34 files changed, 300 insertions(+), 475 deletions(-)

diff --git a/src/main/java/org/variantsync/diffdetective/diff/DiffLineNumber.java b/src/main/java/org/variantsync/diffdetective/diff/DiffLineNumber.java
index bd103243f..048a845cf 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;
 
@@ -77,6 +78,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 +117,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<DiffNode> 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..a6c34112d 100644
--- a/src/main/java/org/variantsync/diffdetective/diff/difftree/DiffNode.java
+++ b/src/main/java/org/variantsync/diffdetective/diff/difftree/DiffNode.java
@@ -9,9 +9,10 @@
 import org.variantsync.diffdetective.util.fide.FixTrueFalse;
 
 import java.util.*;
-import java.util.function.Function;
 import java.util.stream.Collectors;
 
+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;
 
 /**
@@ -45,29 +46,22 @@ public class DiffNode {
     private List<String> lines;
 
     /**
-     * The parent {@link DiffNode} before the edit.
+     * The parents {@link DiffNode} before and after the edit.
+     * This array has to be indexed by {@code Time.ordinal()}
      *
-     * Invariant: Iff {@code beforeParent != null} then
-     * {@code beforeParent.childOrder.contains(this)}.
+     * Invariant: Iff {@code getParent(time) != null} then
+     * {@code getParent(time).childOrder.contains(this)}.
      */
-    private DiffNode beforeParent;
-
-    /**
-     * The parent {@link DiffNode} after the edit.
-     *
-     * Invariant: Iff {@code afterParent != null} then
-     * {@code afterParent.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<DiffNode> childOrder;
 
@@ -169,87 +163,47 @@ 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
+     * 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 DiffNode getBeforeIfNode() {
+    public DiffNode getIfNode(Time time) {
         if (isIf()) {
             return this;
         }
         if (isRoot()) {
             return null;
         }
-        return beforeParent.getBeforeIfNode();
+        return getParent(time).getIfNode(time);
     }
 
     /**
-     * Gets the first if node in the path following the after parent
-     * @return The first if node in the path following the after parent
+     * Gets the number of annotations in the path from the root to this node at the time
+     * {@code time}.
+     * @return the number of annotations above this node at the time {@code time}
      */
-    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(){
+    public int getAnnotationDepth(Time time) {
         if (isRoot()) {
             return 0;
         }
 
         if (isIf()) {
-            return beforeParent.getBeforeAnnotationDepth() + 1;
+            return getParent(time).getAnnotationDepth(time) + 1;
         }
 
-        return beforeParent.getBeforeAnnotationDepth();
+        return getParent(time).getAnnotationDepth(time);
     }
 
     /**
-     * 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 getAfterAnnotationDepth(){
+    public int getDepth(Time time) {
         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
-     */
-    public int getBeforeDepth(){
-        if (isRoot()) {
-            return 0;
-        }
-
-        return beforeParent.getBeforeDepth() + 1;
-    }
-
-    /**
-     * Gets the depth of the diff tree following the after parent
-     * @return the depth of the diff tree following the after parent
-     */
-    public int getAfterDepth(){
-        if (isRoot()) {
-            return 0;
-        }
-
-        return afterParent.getAfterDepth() + 1;
+        return getParent(time).getDepth(time) + 1;
     }
 
     /**
@@ -257,13 +211,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,59 +231,36 @@ 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;
     }
 
     /**
@@ -342,10 +273,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 +286,20 @@ public boolean addBelow(final DiffNode newBeforeParent, final DiffNode newAfterP
      * Inverse of addBelow.
      */
     public void drop() {
-        if (beforeParent != null) {
-            beforeParent.removeBeforeChild(this);
+        if (getParent(BEFORE) != null) {
+            getParent(BEFORE).removeChild(this, BEFORE);
         }
-        if (afterParent != null) {
-            afterParent.removeAfterChild(this);
+        if (getParent(AFTER) != null) {
+            getParent(AFTER).removeChild(this, AFTER);
         }
     }
 
-    private void dropBeforeChild(final DiffNode child) {
-        Assert.assertTrue(child.beforeParent == this);
-        child.beforeParent = null;
-    }
-
-    private void dropAfterChild(final DiffNode child) {
-        Assert.assertTrue(child.afterParent == this);
-        child.afterParent = null;
+    /**
+     * Remove this as the parent of {@code child}.
+     */
+    private void dropChild(final DiffNode child, Time time) {
+        Assert.assertTrue(child.getParent(time) == this);
+        child.parents[time.ordinal()] = null;
     }
 
     /**
@@ -382,132 +311,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
+     * Insert {@code child} as child at the time {@code time} at the position {@code index}.
      */
-    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()) {
+    public boolean insertChild(final DiffNode child, int index, Time time) {
+        if (child.getDiffType().existsAtTime(time)) {
             if (!isChild(child)) {
                 childOrder.add(index, child);
             }
-            child.setBeforeParent(this);
+            child.setParent(this, time);
             return true;
         }
         return false;
     }
 
     /**
-     * The same as {@link DiffNode#insertChildAt} but the time fixed to AFTER.
-     */
-    public boolean insertAfterChild(final DiffNode child, int index) {
-        if (!child.isRem()) {
-            if (!isChild(child)) {
-                childOrder.add(index, child);
-            }
-            child.setAfterParent(this);
-            return true;
-        }
-        return false;
-    }
-
-    /**
-     * The same as {@link DiffNode#insertBeforeChild} 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 + ")!");
-            }
-
-            if (!isChild(child)) {
-                childOrder.add(child);
-            }
-            child.setBeforeParent(this);
-            return true;
-        }
-        return false;
-    }
-
-    /**
-     * The same as {@link DiffNode#insertAfterChild} 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 addAfterChild(final DiffNode child) {
-        if (!child.isRem()) {
-            if (child.afterParent != null) {
-                throw new IllegalArgumentException("Given child " + child + " already has an after parent (" + child.afterParent + ")!");
+    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.setAfterParent(this);
+            child.setParent(this, time);
             return true;
         }
         return false;
     }
 
     /**
-     * Adds all given nodes as before children using {@link DiffNode#addBeforeChild}.
-     * @param beforeChildren Nodes to add as children before the edit.
+     * 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 void addBeforeChildren(final Collection<DiffNode> beforeChildren) {
-        for (final DiffNode beforeChild : beforeChildren) {
-            addBeforeChild(beforeChild);
+    public void addChildren(final Collection<DiffNode> children, Time time) {
+        for (final DiffNode child : children) {
+            addChild(child, time);
         }
     }
 
     /**
-     * Adds all given nodes as after children using {@link DiffNode#addAfterChild}.
-     * @param afterChildren Nodes to add as children after 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 addAfterChildren(final Collection<DiffNode> 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 +377,30 @@ public boolean removeAfterChild(final DiffNode child) {
      */
     public void removeChildren(final Collection<DiffNode> childrenToRemove) {
         for (final DiffNode childToRemove : childrenToRemove) {
-            removeBeforeChild(childToRemove);
-            removeAfterChild(childToRemove);
+            removeChild(childToRemove, BEFORE);
+            removeChild(childToRemove, AFTER);
         }
     }
 
     /**
-     * Removes all children before the edit.
+     * Removes all children before or after the edit.
      * Afterwards, this node will have no before children.
-     * @return All removed before children.
+     * @param time whether to remove all children before or after the edit
+     * @return All removed children.
      */
-    public List<DiffNode> removeBeforeChildren() {
+    public List<DiffNode> removeChildren(Time time) {
         final List<DiffNode> 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)) {
+            if (!isChild(child, time)) {
                 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<DiffNode> removeAfterChildren() {
-        final List<DiffNode> 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)) {
-                return false;
-            }
-
-            orphans.add(child);
-            dropAfterChild(child);
-            return !isBeforeChild(child);
+            dropChild(child, time);
+            return !isChild(child, time.other());
         });
 
         return orphans;
@@ -593,22 +426,15 @@ 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;
+        addChildren(other.removeChildren(BEFORE), BEFORE);
+        addChildren(other.removeChildren(AFTER), AFTER);
     }
 
     /**
-     * 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 +469,11 @@ public Lines getLinesInDiff() {
     }
 
     /**
-     * Returns the range of line numbers of this node's corresponding source code before the edit.
-     * @see DiffLineNumber#rangeBeforeEdit
-     */
-    public Lines getLinesBeforeEdit() {
-        return DiffLineNumber.rangeBeforeEdit(from, to);
-    }
-
-    /**
-     * 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 Lines getLinesAtTime(Time time) {
+        return DiffLineNumber.rangeAtTime(from, to, time);
     }
 
     /**
@@ -689,14 +507,12 @@ public List<DiffNode> 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.
+     * @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.
      *         The returned list represents a conjunction (i.e., all clauses should be combined with boolean AND).
      */
-    private List<Node> getFeatureMappingClauses(final Function<DiffNode, DiffNode> parentOf) {
-        final DiffNode parent = parentOf.apply(this);
+    private List<Node> getFeatureMappingClauses(Time time) {
+        final DiffNode parent = getParent(time);
 
         if (isElse() || isElif()) {
             List<Node> and = new ArrayList<>();
@@ -714,95 +530,57 @@ private List<Node> getFeatureMappingClauses(final Function<DiffNode, DiffNode> p
                     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);
+                ancestor = ancestor.getParent(time);
             }
             and.add(negate(ancestor.getDirectFeatureMapping()));
 
             return and;
         } else if (isArtifact()) {
-            return parent.getFeatureMappingClauses(parentOf);
+            return parent.getFeatureMappingClauses(time);
         }
 
         return List.of(getDirectFeatureMapping());
     }
 
     /**
-     * Same as {@link DiffNode#getFeatureMappingClauses} but conjuncts the returned clauses to a single formula.
+     * Depending on the given time, returns either the feature mapping before or after the edit.
      */
-    private Node getFeatureMapping(Function<DiffNode, DiffNode> parentOf) {
-        final List<Node> fmClauses = getFeatureMappingClauses(parentOf);
+    public Node getFeatureMapping(Time time) {
+        final List<Node> fmClauses = getFeatureMappingClauses(time);
         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).
-     * @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.
+     * @param time Whether to return the presence condition clauses 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<Node> getPresenceCondition(Function<DiffNode, DiffNode> parentOf) {
-        final DiffNode parent = parentOf.apply(this);
+    private List<Node> getPresenceConditionClauses(Time time) {
+        final DiffNode parent = getParent(time);
 
         if (isElse() || isElif()) {
-            final List<Node> clauses = new ArrayList<>(getFeatureMappingClauses(parentOf));
+            final List<Node> clauses = new ArrayList<>(getFeatureMappingClauses(time));
 
             // Find corresponding if
             DiffNode correspondingIf = parent;
             while (!correspondingIf.isIf()) {
-                correspondingIf = parentOf.apply(correspondingIf);
+                correspondingIf = correspondingIf.getParent(time);
             }
 
             // If this elif-else-chain was again nested in another annotation, add its pc.
-            final DiffNode outerNesting = parentOf.apply(correspondingIf);
+            final DiffNode outerNesting = correspondingIf.getParent(time);
             if (outerNesting != null) {
-                clauses.addAll(outerNesting.getPresenceCondition(parentOf));
+                clauses.addAll(outerNesting.getPresenceConditionClauses(time));
             }
 
             return clauses;
         } else if (isArtifact()) {
-            return parent.getPresenceCondition(parentOf);
+            return parent.getPresenceConditionClauses(time);
         }
 
         // this is mapping or root
@@ -810,76 +588,38 @@ private List<Node> getPresenceCondition(Function<DiffNode, DiffNode> parentOf) {
         if (parent == null) {
             clauses = new ArrayList<>(1);
         } else {
-            clauses = parent.getPresenceCondition(parentOf);
+            clauses = parent.getPresenceConditionClauses(time);
         }
         clauses.add(featureMapping);
         return clauses;
     }
 
     /**
-     * Returns the presence condition of this node before 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 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.
+     * 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 getAfterPresenceCondition() {
-        if (diffType.existsAfter()) {
-            return new And(getPresenceCondition(DiffNode::getAfterParent));
+    public Node getPresenceCondition(Time time) {
+        if (diffType.existsAtTime(time)) {
+            return new And(getPresenceConditionClauses(time));
         } 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;
-    }
-
     /**
      * 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;
     }
 
     /**
@@ -956,7 +696,7 @@ public boolean isElse() {
      * Returns true if this node is a root node (has no parents).
      */
     public boolean isRoot() {
-        return getBeforeParent() == null && getAfterParent() == null;
+        return getParent(BEFORE) == null && getParent(AFTER) == null;
     }
 
     /**
@@ -1026,33 +766,33 @@ 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.getParent(BEFORE) != null) {
+                Assert.assertTrue(c.getParent(BEFORE).isChild(c, BEFORE), () -> "The parent before the edit 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");
+            if (c.getParent(AFTER) != null) {
+                Assert.assertTrue(c.getParent(AFTER).isChild(c, AFTER), () -> "The parent after 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 (getParent(BEFORE) != null) {
+                Assert.assertTrue(getParent(BEFORE).isIf() || getParent(BEFORE).isElif(), "Before parent " + getParent(BEFORE) + " 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!");
+            if (getParent(AFTER) != null) {
+                Assert.assertTrue(getParent(AFTER).isIf() || getParent(AFTER).isElif(), "After parent " + getParent(AFTER) + " of " + this + " is neither IF nor ELIF!");
             }
         }
 
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..a6123548f 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,16 +340,16 @@ public boolean isEmpty() {
     public void removeNode(DiffNode node) {
         Assert.assertTrue(node != root);
 
-        final DiffNode beforeParent = node.getBeforeParent();
+        final DiffNode beforeParent = node.getParent(BEFORE);
         if (beforeParent != null) {
-            beforeParent.removeBeforeChild(node);
-            beforeParent.addBeforeChildren(node.removeBeforeChildren());
+            beforeParent.removeChild(node, BEFORE);
+            beforeParent.addChildren(node.removeChildren(BEFORE), BEFORE);
         }
 
-        final DiffNode afterParent = node.getAfterParent();
+        final DiffNode afterParent = node.getParent(AFTER);
         if (afterParent != null) {
-            afterParent.removeAfterChild(node);
-            afterParent.addAfterChildren(node.removeAfterChildren());
+            afterParent.removeChild(node, AFTER);
+            afterParent.addChildren(node.removeChildren(AFTER), AFTER);
         }
     }
 
@@ -394,8 +396,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/Time.java b/src/main/java/org/variantsync/diffdetective/diff/difftree/Time.java
index 98306052e..df55931f1 100644
--- a/src/main/java/org/variantsync/diffdetective/diff/difftree/Time.java
+++ b/src/main/java/org/variantsync/diffdetective/diff/difftree/Time.java
@@ -49,4 +49,15 @@ public <T> T match(final T before, final T after) {
             case AFTER -> after;
         };
     }
+
+    /**
+     * Returns the complement of this time.
+     * @return {@code BEFORE} if {@code this == AFTER} or {@code AFTER} if {@code this == BEFORE}
+     */
+    public Time other() {
+        return switch (this) {
+            case BEFORE -> AFTER;
+            case AFTER -> BEFORE;
+        };
+    }
 }
diff --git a/src/main/java/org/variantsync/diffdetective/diff/difftree/serialize/Format.java b/src/main/java/org/variantsync/diffdetective/diff/difftree/serialize/Format.java
index 66b5fb156..121a9a063 100644
--- a/src/main/java/org/variantsync/diffdetective/diff/difftree/serialize/Format.java
+++ b/src/main/java/org/variantsync/diffdetective/diff/difftree/serialize/Format.java
@@ -7,6 +7,9 @@
 import org.variantsync.diffdetective.diff.difftree.serialize.edgeformat.EdgeLabelFormat;
 import org.variantsync.diffdetective.diff.difftree.serialize.nodeformat.DiffNodeLabelFormat;
 
+import static org.variantsync.diffdetective.diff.difftree.Time.AFTER;
+import static org.variantsync.diffdetective.diff.difftree.Time.BEFORE;
+
 /**
  * Format used for exporting a {@link DiffTree}.
  * For easy reusability this class is composed of separate node and edge formats.
@@ -84,8 +87,8 @@ public void forEachEdge(DiffTree diffTree, Consumer<StyledEdge> callback) {
      */
     public void forEachUniqueEdge(DiffTree diffTree, Consumer<List<StyledEdge>> callback) {
         diffTree.forAll((node) -> {
-            var beforeParent = node.getBeforeParent();
-            var afterParent = node.getAfterParent();
+            var beforeParent = node.getParent(BEFORE);
+            var afterParent = node.getParent(AFTER);
 
             // Are both parent edges the same?
             if (beforeParent != null && afterParent != null && beforeParent == afterParent) {
@@ -108,7 +111,7 @@ public void forEachUniqueEdge(DiffTree diffTree, Consumer<List<StyledEdge>> call
      * of {@link getEdgeFormat()}.
      */
     protected StyledEdge beforeEdge(DiffNode node) {
-        return sortedEdgeWithLabel(node, node.getBeforeParent(), StyledEdge.BEFORE);
+        return sortedEdgeWithLabel(node, node.getParent(BEFORE), StyledEdge.BEFORE);
     }
 
     /**
@@ -118,7 +121,7 @@ protected StyledEdge beforeEdge(DiffNode node) {
      * of {@link getEdgeFormat()}.
      */
     protected StyledEdge afterEdge(DiffNode node) {
-        return sortedEdgeWithLabel(node, node.getAfterParent(), StyledEdge.AFTER);
+        return sortedEdgeWithLabel(node, node.getParent(AFTER), StyledEdge.AFTER);
     }
 
     /**
diff --git a/src/main/java/org/variantsync/diffdetective/diff/difftree/serialize/edgeformat/EdgeLabelFormat.java b/src/main/java/org/variantsync/diffdetective/diff/difftree/serialize/edgeformat/EdgeLabelFormat.java
index 3ab75ce95..5b9c10925 100644
--- a/src/main/java/org/variantsync/diffdetective/diff/difftree/serialize/edgeformat/EdgeLabelFormat.java
+++ b/src/main/java/org/variantsync/diffdetective/diff/difftree/serialize/edgeformat/EdgeLabelFormat.java
@@ -10,6 +10,9 @@
 import java.util.List;
 import java.util.Map;
 
+import static org.variantsync.diffdetective.diff.difftree.Time.AFTER;
+import static org.variantsync.diffdetective.diff.difftree.Time.BEFORE;
+
 /**
  * Reads and writes edges between {@link DiffNode DiffNodes} from and to line graph.
  *
@@ -83,7 +86,7 @@ public Direction getEdgeDirection() {
 
     /**
      * Adds the given child node as the child of the given parent node for all times described by the given label
-     * (via {@link DiffNode#addBeforeChild(DiffNode)} and {@link DiffNode#addAfterChild(DiffNode)})
+     * (via {@link DiffNode#addChild(DiffNode, Time)})
      * In particular, this method checks if the given edge label starts with
      * {@link LineGraphConstants#BEFORE_PARENT}, {@link LineGraphConstants#AFTER_PARENT}, or
      * {@link LineGraphConstants#BEFORE_AND_AFTER_PARENT} and connects both nodes accordingly.
@@ -95,14 +98,14 @@ public Direction getEdgeDirection() {
     protected void connectAccordingToLabel(final DiffNode child, final DiffNode parent, final String edgeLabel) {
         if (edgeLabel.startsWith(LineGraphConstants.BEFORE_AND_AFTER_PARENT)) {
             // Nothing has been changed. The child-parent relationship remains the same
-            Assert.assertTrue(parent.addAfterChild(child));
-            Assert.assertTrue(parent.addBeforeChild(child));
+            Assert.assertTrue(parent.addChild(child, AFTER));
+            Assert.assertTrue(parent.addChild(child, BEFORE));
         } else if (edgeLabel.startsWith(LineGraphConstants.BEFORE_PARENT)) {
             // The child DiffNode lost its parent DiffNode (an orphan DiffNode)
-            Assert.assertTrue(parent.addBeforeChild(child));
+            Assert.assertTrue(parent.addChild(child, BEFORE));
         } else if (edgeLabel.startsWith(LineGraphConstants.AFTER_PARENT)) {
             // The parent DiffNode has a new child DiffNode
-            Assert.assertTrue(parent.addAfterChild(child));
+            Assert.assertTrue(parent.addChild(child, AFTER));
         } else {
             throw new IllegalArgumentException("Syntax error. Invalid name in edge label " + edgeLabel);
         }
diff --git a/src/main/java/org/variantsync/diffdetective/diff/difftree/transform/CollapseNestedNonEditedAnnotations.java b/src/main/java/org/variantsync/diffdetective/diff/difftree/transform/CollapseNestedNonEditedAnnotations.java
index 737840218..9fbf5760d 100644
--- a/src/main/java/org/variantsync/diffdetective/diff/difftree/transform/CollapseNestedNonEditedAnnotations.java
+++ b/src/main/java/org/variantsync/diffdetective/diff/difftree/transform/CollapseNestedNonEditedAnnotations.java
@@ -13,6 +13,9 @@
 import java.util.List;
 import java.util.Stack;
 
+import static org.variantsync.diffdetective.diff.difftree.Time.AFTER;
+import static org.variantsync.diffdetective.diff.difftree.Time.BEFORE;
+
 /**
  * Collapses chains of nested non-edited annotations.
  * Imagine a annotation node that is unchanged and has the same parent before and after the edit
@@ -72,7 +75,7 @@ private void findChains(DiffTreeTraversal traversal, DiffNode subtree) {
                 s.push(subtree);
                 chainCandidates.add(s);
             } else if (inChainTail(subtree)) {
-                final DiffNode parent = subtree.getBeforeParent(); // == after parent
+                final DiffNode parent = subtree.getParent(BEFORE); // == after parent
 
                 Stack<DiffNode> pushedTo = null;
                 for (final Stack<DiffNode> s : chainCandidates) {
@@ -104,9 +107,9 @@ private static void collapseChain(Stack<DiffNode> chain) {
 
             switch (lastPopped.nodeType) {
                 case IF ->
-                    featureMappings.add(lastPopped.getAfterFeatureMapping());
+                    featureMappings.add(lastPopped.getFeatureMapping(AFTER));
                 case ELSE, ELIF -> {
-                    featureMappings.add(lastPopped.getAfterFeatureMapping());
+                    featureMappings.add(lastPopped.getFeatureMapping(AFTER));
                     // Pop all previous ELIF cases and the final IF (if present) as we accounted
                     // for their features mappings already.
                     while (!lastPopped.isIf() && !chain.isEmpty()) {
@@ -120,8 +123,8 @@ private static void collapseChain(Stack<DiffNode> chain) {
 
         Assert.assertTrue(head == lastPopped);
 
-        final DiffNode beforeParent = head.getBeforeParent();
-        final DiffNode afterParent = head.getAfterParent();
+        final DiffNode beforeParent = head.getParent(BEFORE);
+        final DiffNode afterParent = head.getParent(AFTER);
 
         ArrayList lines = new ArrayList();
         lines.add("$Collapsed Nested Annotations$");
@@ -158,14 +161,14 @@ private static boolean hasExactlyOneChild(DiffNode d) {
      * @return True iff d is in the tail of a chain.
      */
     private static boolean inChainTail(DiffNode d) {
-        return d.getBeforeParent() == d.getAfterParent() && hasExactlyOneChild(d.getBeforeParent());
+        return d.getParent(BEFORE) == d.getParent(AFTER) && hasExactlyOneChild(d.getParent(BEFORE));
     }
 
     /**
      * @return True iff d is the head of a chain.
      */
     private static boolean isHead(DiffNode d) {
-        return (!inChainTail(d) || d.getBeforeParent().isRoot()) && !isEnd(d);
+        return (!inChainTail(d) || d.getParent(BEFORE).isRoot()) && !isEnd(d);
     }
 
     /**
diff --git a/src/main/java/org/variantsync/diffdetective/diff/difftree/transform/CutNonEditedSubtrees.java b/src/main/java/org/variantsync/diffdetective/diff/difftree/transform/CutNonEditedSubtrees.java
index cdc954813..f99d3dc44 100644
--- a/src/main/java/org/variantsync/diffdetective/diff/difftree/transform/CutNonEditedSubtrees.java
+++ b/src/main/java/org/variantsync/diffdetective/diff/difftree/transform/CutNonEditedSubtrees.java
@@ -7,6 +7,9 @@
 
 import java.util.ArrayList;
 
+import static org.variantsync.diffdetective.diff.difftree.Time.AFTER;
+import static org.variantsync.diffdetective.diff.difftree.Time.BEFORE;
+
 /**
  * This transformer removes all subtrees from a DiffTree that are non-edited.
  * A subtree is unedited, if all nodes in it are unchanged and all nodes have the same
@@ -49,8 +52,8 @@ public void visit(final DiffTreeTraversal traversal, final DiffNode subtree) {
              */
             if (
                     child.getAllChildren().isEmpty()
-                            && child.getAfterParent() == subtree
-                            && child.getBeforeParent() == subtree)
+                            && child.getParent(AFTER) == subtree
+                            && child.getParent(BEFORE) == subtree)
             {
                 collapsableChildren.add(child);
             }
diff --git a/src/main/java/org/variantsync/diffdetective/diff/difftree/transform/NaiveMovedArtifactDetection.java b/src/main/java/org/variantsync/diffdetective/diff/difftree/transform/NaiveMovedArtifactDetection.java
index 492f17be8..f7ea12ea5 100644
--- a/src/main/java/org/variantsync/diffdetective/diff/difftree/transform/NaiveMovedArtifactDetection.java
+++ b/src/main/java/org/variantsync/diffdetective/diff/difftree/transform/NaiveMovedArtifactDetection.java
@@ -9,6 +9,9 @@
 import java.util.ArrayList;
 import java.util.List;
 
+import static org.variantsync.diffdetective.diff.difftree.Time.AFTER;
+import static org.variantsync.diffdetective.diff.difftree.Time.BEFORE;
+
 /**
  * Finds artifact nodes whose label is exactly equal. If one of those nodes was added and the other one was removed,
  * NaiveMovedArtifactDetection merges them and interprets this edit as a move instead of separate insertion and deletion.
@@ -34,8 +37,8 @@ public void transform(final DiffTree diffTree) {
                 removed = twin.first();
             }
 
-            final DiffNode afterParent = added.getAfterParent();
-            final DiffNode beforeParent = removed.getBeforeParent();
+            final DiffNode afterParent = added.getParent(AFTER);
+            final DiffNode beforeParent = removed.getParent(BEFORE);
             final DiffNode mergedNode = merge(added, removed);
 
             added.drop();
diff --git a/src/main/java/org/variantsync/diffdetective/diff/difftree/transform/Starfold.java b/src/main/java/org/variantsync/diffdetective/diff/difftree/transform/Starfold.java
index 1baae6711..3ab175db1 100644
--- a/src/main/java/org/variantsync/diffdetective/diff/difftree/transform/Starfold.java
+++ b/src/main/java/org/variantsync/diffdetective/diff/difftree/transform/Starfold.java
@@ -89,7 +89,7 @@ private void mergeArms(final DiffNode starRoot, Time time, final DiffType target
         if (starArms.size() > 1) {
             final int targetIndex = starRoot.indexOfChild(starArms.get(0));
             starRoot.removeChildren(starArms);
-            starRoot.insertChildAt(
+            starRoot.insertChild(
                     DiffNode.createArtifact(
                             targetDiffType,
                             DiffLineNumber.Invalid(),
diff --git a/src/main/java/org/variantsync/diffdetective/editclass/proposed/AddToPC.java b/src/main/java/org/variantsync/diffdetective/editclass/proposed/AddToPC.java
index 75eeb9dad..0cc723a52 100644
--- a/src/main/java/org/variantsync/diffdetective/editclass/proposed/AddToPC.java
+++ b/src/main/java/org/variantsync/diffdetective/editclass/proposed/AddToPC.java
@@ -4,6 +4,8 @@
 import org.variantsync.diffdetective.diff.difftree.DiffType;
 import org.variantsync.diffdetective.editclass.EditClass;
 
+import static org.variantsync.diffdetective.diff.difftree.Time.AFTER;
+
 /**
  * Our AddToPC edit class from the ESEC/FSE'22 paper.
  * @author Paul Bittner, Sören Viegener
@@ -15,6 +17,6 @@ final class AddToPC extends EditClass {
 
     @Override
     protected boolean matchesArtifactNode(DiffNode node) {
-        return !node.getAfterParent().isAdd();
+        return !node.getParent(AFTER).isAdd();
     }
 }
diff --git a/src/main/java/org/variantsync/diffdetective/editclass/proposed/AddWithMapping.java b/src/main/java/org/variantsync/diffdetective/editclass/proposed/AddWithMapping.java
index 679a0255e..264bc4fb7 100644
--- a/src/main/java/org/variantsync/diffdetective/editclass/proposed/AddWithMapping.java
+++ b/src/main/java/org/variantsync/diffdetective/editclass/proposed/AddWithMapping.java
@@ -4,6 +4,8 @@
 import org.variantsync.diffdetective.diff.difftree.DiffType;
 import org.variantsync.diffdetective.editclass.EditClass;
 
+import static org.variantsync.diffdetective.diff.difftree.Time.AFTER;
+
 /**
  * Our AddWithMapping edit class from the ESEC/FSE'22 paper.
  * @author Paul Bittner, Sören Viegener
@@ -15,6 +17,6 @@ final class AddWithMapping extends EditClass {
 
     @Override
     protected boolean matchesArtifactNode(DiffNode artifactNode) {
-        return artifactNode.getAfterParent().isAdd();
+        return artifactNode.getParent(AFTER).isAdd();
     }
 }
diff --git a/src/main/java/org/variantsync/diffdetective/editclass/proposed/Generalization.java b/src/main/java/org/variantsync/diffdetective/editclass/proposed/Generalization.java
index 8cfb6e4aa..d91c23d13 100644
--- a/src/main/java/org/variantsync/diffdetective/editclass/proposed/Generalization.java
+++ b/src/main/java/org/variantsync/diffdetective/editclass/proposed/Generalization.java
@@ -6,6 +6,9 @@
 import org.variantsync.diffdetective.diff.difftree.DiffType;
 import org.variantsync.diffdetective.editclass.EditClass;
 
+import static org.variantsync.diffdetective.diff.difftree.Time.AFTER;
+import static org.variantsync.diffdetective.diff.difftree.Time.BEFORE;
+
 /**
  * Our Generalization edit class from the ESEC/FSE'22 paper.
  * @author Paul Bittner, Sören Viegener
@@ -17,8 +20,8 @@ final class Generalization extends EditClass {
 
     @Override
     protected boolean matchesArtifactNode(DiffNode artifactNode) {
-        final Node pcb = artifactNode.getBeforeFeatureMapping();
-        final Node pca = artifactNode.getAfterFeatureMapping();
+        final Node pcb = artifactNode.getFeatureMapping(BEFORE);
+        final Node pca = artifactNode.getFeatureMapping(AFTER);
         return SAT.implies(pcb, pca) && !SAT.implies(pca, pcb);
     }
 }
diff --git a/src/main/java/org/variantsync/diffdetective/editclass/proposed/ProposedEditClasses.java b/src/main/java/org/variantsync/diffdetective/editclass/proposed/ProposedEditClasses.java
index f8a3f278a..c26b479a4 100644
--- a/src/main/java/org/variantsync/diffdetective/editclass/proposed/ProposedEditClasses.java
+++ b/src/main/java/org/variantsync/diffdetective/editclass/proposed/ProposedEditClasses.java
@@ -10,6 +10,9 @@
 
 import java.util.*;
 
+import static org.variantsync.diffdetective.diff.difftree.Time.AFTER;
+import static org.variantsync.diffdetective.diff.difftree.Time.BEFORE;
+
 /**
  * The catalog of edit classes proposed in our ESEC/FSE'22 paper.
  * @author Paul Bittner
@@ -76,13 +79,13 @@ public EditClass match(DiffNode node)
         }
 
         if (node.isAdd()) {
-            if (node.getAfterParent().isAdd()) {
+            if (node.getParent(AFTER).isAdd()) {
                 return AddWithMapping;
             } else {
                 return AddToPC;
             }
         } else if (node.isRem()) {
-            if (node.getBeforeParent().isRem()) {
+            if (node.getParent(BEFORE).isRem()) {
                 return RemWithMapping;
             } else {
                 return RemFromPC;
@@ -90,8 +93,8 @@ public EditClass match(DiffNode node)
         } else {
             Assert.assertTrue(node.isNon());
 
-            final Node pcb = node.getBeforePresenceCondition();
-            final Node pca = node.getAfterPresenceCondition();
+            final Node pcb = node.getPresenceCondition(BEFORE);
+            final Node pca = node.getPresenceCondition(AFTER);
 
             final boolean beforeVariantsSubsetOfAfterVariants;
             final boolean afterVariantsSubsetOfBeforeVariants;
diff --git a/src/main/java/org/variantsync/diffdetective/editclass/proposed/Reconfiguration.java b/src/main/java/org/variantsync/diffdetective/editclass/proposed/Reconfiguration.java
index cbe7de735..385494a95 100644
--- a/src/main/java/org/variantsync/diffdetective/editclass/proposed/Reconfiguration.java
+++ b/src/main/java/org/variantsync/diffdetective/editclass/proposed/Reconfiguration.java
@@ -6,6 +6,9 @@
 import org.variantsync.diffdetective.diff.difftree.DiffType;
 import org.variantsync.diffdetective.editclass.EditClass;
 
+import static org.variantsync.diffdetective.diff.difftree.Time.AFTER;
+import static org.variantsync.diffdetective.diff.difftree.Time.BEFORE;
+
 /**
  * Our Reconfiguration edit class from the ESEC/FSE'22 paper.
  * @author Paul Bittner, Sören Viegener
@@ -17,8 +20,8 @@ final class Reconfiguration extends EditClass {
 
     @Override
     protected boolean matchesArtifactNode(DiffNode artifactNode) {
-        final Node pcb = artifactNode.getBeforeFeatureMapping();
-        final Node pca = artifactNode.getAfterFeatureMapping();
+        final Node pcb = artifactNode.getFeatureMapping(BEFORE);
+        final Node pca = artifactNode.getFeatureMapping(AFTER);
         return !SAT.implies(pcb, pca) && !SAT.implies(pca, pcb);
     }
 }
diff --git a/src/main/java/org/variantsync/diffdetective/editclass/proposed/Refactoring.java b/src/main/java/org/variantsync/diffdetective/editclass/proposed/Refactoring.java
index e2a2c82ca..66ee3300d 100644
--- a/src/main/java/org/variantsync/diffdetective/editclass/proposed/Refactoring.java
+++ b/src/main/java/org/variantsync/diffdetective/editclass/proposed/Refactoring.java
@@ -6,6 +6,9 @@
 import org.variantsync.diffdetective.diff.difftree.DiffType;
 import org.variantsync.diffdetective.editclass.EditClass;
 
+import static org.variantsync.diffdetective.diff.difftree.Time.AFTER;
+import static org.variantsync.diffdetective.diff.difftree.Time.BEFORE;
+
 /**
  * Our Refactoring edit class from the ESEC/FSE'22 paper.
  * @author Paul Bittner, Sören Viegener
@@ -17,8 +20,8 @@ final class Refactoring extends EditClass {
 
     @Override
     protected boolean matchesArtifactNode(DiffNode artifactNode) {
-        final Node pcb = artifactNode.getBeforeFeatureMapping();
-        final Node pca = artifactNode.getAfterFeatureMapping();
+        final Node pcb = artifactNode.getFeatureMapping(BEFORE);
+        final Node pca = artifactNode.getFeatureMapping(AFTER);
         return SAT.equivalent(pcb, pca) && !artifactNode.beforePathEqualsAfterPath();
     }
 }
diff --git a/src/main/java/org/variantsync/diffdetective/editclass/proposed/RemFromPC.java b/src/main/java/org/variantsync/diffdetective/editclass/proposed/RemFromPC.java
index 436684ccc..e82594c0e 100644
--- a/src/main/java/org/variantsync/diffdetective/editclass/proposed/RemFromPC.java
+++ b/src/main/java/org/variantsync/diffdetective/editclass/proposed/RemFromPC.java
@@ -4,6 +4,8 @@
 import org.variantsync.diffdetective.diff.difftree.DiffType;
 import org.variantsync.diffdetective.editclass.EditClass;
 
+import static org.variantsync.diffdetective.diff.difftree.Time.BEFORE;
+
 /**
  * Our RemFromPC edit class from the ESEC/FSE'22 paper.
  * @author Paul Bittner, Sören Viegener
@@ -15,6 +17,6 @@ final class RemFromPC extends EditClass {
 
     @Override
     protected boolean matchesArtifactNode(DiffNode artifactNode) {
-        return !artifactNode.getBeforeParent().isRem();
+        return !artifactNode.getParent(BEFORE).isRem();
     }
 }
diff --git a/src/main/java/org/variantsync/diffdetective/editclass/proposed/RemWithMapping.java b/src/main/java/org/variantsync/diffdetective/editclass/proposed/RemWithMapping.java
index a527e7f9d..9f803bb94 100644
--- a/src/main/java/org/variantsync/diffdetective/editclass/proposed/RemWithMapping.java
+++ b/src/main/java/org/variantsync/diffdetective/editclass/proposed/RemWithMapping.java
@@ -4,6 +4,8 @@
 import org.variantsync.diffdetective.diff.difftree.DiffType;
 import org.variantsync.diffdetective.editclass.EditClass;
 
+import static org.variantsync.diffdetective.diff.difftree.Time.BEFORE;
+
 /**
  * Our RemWithMapping edit class from the ESEC/FSE'22 paper.
  * @author Paul Bittner, Sören Viegener
@@ -15,6 +17,6 @@ final class RemWithMapping extends EditClass {
 
     @Override
     protected boolean matchesArtifactNode(DiffNode artifactNode) {
-        return artifactNode.getBeforeParent().isRem();
+        return artifactNode.getParent(BEFORE).isRem();
     }
 }
diff --git a/src/main/java/org/variantsync/diffdetective/editclass/proposed/Specialization.java b/src/main/java/org/variantsync/diffdetective/editclass/proposed/Specialization.java
index 0ef55ac55..0be909f11 100644
--- a/src/main/java/org/variantsync/diffdetective/editclass/proposed/Specialization.java
+++ b/src/main/java/org/variantsync/diffdetective/editclass/proposed/Specialization.java
@@ -6,6 +6,9 @@
 import org.variantsync.diffdetective.diff.difftree.DiffType;
 import org.variantsync.diffdetective.editclass.EditClass;
 
+import static org.variantsync.diffdetective.diff.difftree.Time.AFTER;
+import static org.variantsync.diffdetective.diff.difftree.Time.BEFORE;
+
 /**
  * Our Specialization edit class from the ESEC/FSE'22 paper.
  * @author Paul Bittner, Sören Viegener
@@ -17,8 +20,8 @@ final class Specialization extends EditClass {
 
     @Override
     protected boolean matchesArtifactNode(DiffNode artifactNode) {
-        final Node pcb = artifactNode.getBeforeFeatureMapping();
-        final Node pca = artifactNode.getAfterFeatureMapping();
+        final Node pcb = artifactNode.getFeatureMapping(BEFORE);
+        final Node pca = artifactNode.getFeatureMapping(AFTER);
         return !SAT.implies(pcb, pca) && SAT.implies(pca, pcb);
     }
 }
diff --git a/src/main/java/org/variantsync/diffdetective/editclass/proposed/Untouched.java b/src/main/java/org/variantsync/diffdetective/editclass/proposed/Untouched.java
index a5bef0e7e..bf7ef41b7 100644
--- a/src/main/java/org/variantsync/diffdetective/editclass/proposed/Untouched.java
+++ b/src/main/java/org/variantsync/diffdetective/editclass/proposed/Untouched.java
@@ -6,6 +6,9 @@
 import org.variantsync.diffdetective.diff.difftree.DiffType;
 import org.variantsync.diffdetective.editclass.EditClass;
 
+import static org.variantsync.diffdetective.diff.difftree.Time.AFTER;
+import static org.variantsync.diffdetective.diff.difftree.Time.BEFORE;
+
 /**
  * Our Untouched edit class from the ESEC/FSE'22 paper.
  * @author Paul Bittner, Sören Viegener
@@ -17,8 +20,8 @@ final class Untouched extends EditClass {
 
     @Override
     protected boolean matchesArtifactNode(DiffNode artifactNode) {
-        final Node pcb = artifactNode.getBeforeFeatureMapping();
-        final Node pca = artifactNode.getAfterFeatureMapping();
+        final Node pcb = artifactNode.getFeatureMapping(BEFORE);
+        final Node pca = artifactNode.getFeatureMapping(AFTER);
         return SAT.equivalent(pcb, pca) && artifactNode.beforePathEqualsAfterPath();
     }
 }
diff --git a/src/main/java/org/variantsync/diffdetective/mining/RunningExampleFinder.java b/src/main/java/org/variantsync/diffdetective/mining/RunningExampleFinder.java
index 15367bc4d..ed909ee05 100644
--- a/src/main/java/org/variantsync/diffdetective/mining/RunningExampleFinder.java
+++ b/src/main/java/org/variantsync/diffdetective/mining/RunningExampleFinder.java
@@ -19,6 +19,8 @@
 import java.nio.file.Path;
 import java.util.Optional;
 
+import static org.variantsync.diffdetective.diff.difftree.Time.BEFORE;
+
 public class RunningExampleFinder {
     public static final Path DefaultExamplesDirectory = Path.of("examples");
     public static final int DefaultMaxDiffLineCount = 20;
@@ -77,8 +79,8 @@ private static boolean hasAnnotatedMacros(final DiffTree diffTree) {
     private static boolean hasNestingBeforeEdit(final DiffTree diffTree) {
         return diffTree.anyMatch(n ->
                            !n.isAdd()
-                        && n.getBeforeDepth() > 2
-                        && !(n.getBeforeParent().isElse() || n.getBeforeParent().isElif())
+                        && n.getDepth(BEFORE) > 2
+                        && !(n.getParent(BEFORE).isElse() || n.getParent(BEFORE).isElif())
         );
     }
 
diff --git a/src/main/java/org/variantsync/diffdetective/preliminary/pattern/elementary/FeatureContextOfAddToPC.java b/src/main/java/org/variantsync/diffdetective/preliminary/pattern/elementary/FeatureContextOfAddToPC.java
index d88b2a11b..0af139088 100644
--- a/src/main/java/org/variantsync/diffdetective/preliminary/pattern/elementary/FeatureContextOfAddToPC.java
+++ b/src/main/java/org/variantsync/diffdetective/preliminary/pattern/elementary/FeatureContextOfAddToPC.java
@@ -9,6 +9,8 @@
 import org.variantsync.diffdetective.preliminary.evaluation.FeatureContext;
 import org.variantsync.diffdetective.preliminary.pattern.FeatureContextReverseEngineering;
 
+import static org.variantsync.diffdetective.diff.difftree.Time.AFTER;
+
 @Deprecated
 public final class FeatureContextOfAddToPC implements FeatureContextReverseEngineering<DiffNode> {
     @Override
@@ -18,7 +20,7 @@ public Pattern<DiffNode> getPattern() {
 
     @Override
     public PatternMatch<DiffNode> createMatch(DiffNode codeNode) {
-        final Node fm = codeNode.getAfterParent().getAfterFeatureMapping();
+        final Node fm = codeNode.getParent(AFTER).getFeatureMapping(AFTER);
         final Lines diffLines = codeNode.getLinesInDiff();
 
         return new PatternMatch<>(this,
diff --git a/src/main/java/org/variantsync/diffdetective/preliminary/pattern/elementary/FeatureContextOfAddWithMapping.java b/src/main/java/org/variantsync/diffdetective/preliminary/pattern/elementary/FeatureContextOfAddWithMapping.java
index 883ec9177..7a54a1f90 100644
--- a/src/main/java/org/variantsync/diffdetective/preliminary/pattern/elementary/FeatureContextOfAddWithMapping.java
+++ b/src/main/java/org/variantsync/diffdetective/preliminary/pattern/elementary/FeatureContextOfAddWithMapping.java
@@ -9,6 +9,8 @@
 import org.variantsync.diffdetective.preliminary.evaluation.FeatureContext;
 import org.variantsync.diffdetective.preliminary.pattern.FeatureContextReverseEngineering;
 
+import static org.variantsync.diffdetective.diff.difftree.Time.AFTER;
+
 @Deprecated
 public final class FeatureContextOfAddWithMapping implements FeatureContextReverseEngineering<DiffNode> {
     @Override
@@ -18,7 +20,7 @@ public Pattern<DiffNode> getPattern() {
 
     @Override
     public PatternMatch<DiffNode> createMatch(DiffNode codeNode) {
-        final Node fm = codeNode.getAfterParent().getAfterFeatureMapping();
+        final Node fm = codeNode.getParent(AFTER).getFeatureMapping(AFTER);
         final Lines diffLines = codeNode.getLinesInDiff();
 
         return new PatternMatch<>(this,
diff --git a/src/main/java/org/variantsync/diffdetective/preliminary/pattern/elementary/FeatureContextOfRemFromPC.java b/src/main/java/org/variantsync/diffdetective/preliminary/pattern/elementary/FeatureContextOfRemFromPC.java
index d8ab176fe..ff2b1d7f8 100644
--- a/src/main/java/org/variantsync/diffdetective/preliminary/pattern/elementary/FeatureContextOfRemFromPC.java
+++ b/src/main/java/org/variantsync/diffdetective/preliminary/pattern/elementary/FeatureContextOfRemFromPC.java
@@ -9,6 +9,8 @@
 import org.variantsync.diffdetective.preliminary.evaluation.FeatureContext;
 import org.variantsync.diffdetective.preliminary.pattern.FeatureContextReverseEngineering;
 
+import static org.variantsync.diffdetective.diff.difftree.Time.BEFORE;
+
 @Deprecated
 public final class FeatureContextOfRemFromPC implements FeatureContextReverseEngineering<DiffNode> {
     @Override
@@ -18,7 +20,7 @@ public Pattern<DiffNode> getPattern() {
 
     @Override
     public PatternMatch<DiffNode> createMatch(DiffNode codeNode) {
-        final Node fm = codeNode.getBeforeParent().getBeforeFeatureMapping();
+        final Node fm = codeNode.getParent(BEFORE).getFeatureMapping(BEFORE);
         final Lines diffLines = codeNode.getLinesInDiff();
 
         return new PatternMatch<>(this,
diff --git a/src/main/java/org/variantsync/diffdetective/preliminary/pattern/elementary/FeatureContextOfRemWithMapping.java b/src/main/java/org/variantsync/diffdetective/preliminary/pattern/elementary/FeatureContextOfRemWithMapping.java
index af21ac27e..bcccd6a3c 100644
--- a/src/main/java/org/variantsync/diffdetective/preliminary/pattern/elementary/FeatureContextOfRemWithMapping.java
+++ b/src/main/java/org/variantsync/diffdetective/preliminary/pattern/elementary/FeatureContextOfRemWithMapping.java
@@ -9,6 +9,8 @@
 import org.variantsync.diffdetective.preliminary.evaluation.FeatureContext;
 import org.variantsync.diffdetective.preliminary.pattern.FeatureContextReverseEngineering;
 
+import static org.variantsync.diffdetective.diff.difftree.Time.BEFORE;
+
 @Deprecated
 public final class FeatureContextOfRemWithMapping implements FeatureContextReverseEngineering<DiffNode> {
     @Override
@@ -18,7 +20,7 @@ public Pattern<DiffNode> getPattern() {
 
     @Override
     public PatternMatch<DiffNode> createMatch(DiffNode codeNode) {
-        final Node fm = codeNode.getBeforeParent().getBeforeFeatureMapping();
+        final Node fm = codeNode.getParent(BEFORE).getFeatureMapping(BEFORE);
         final Lines diffLines = codeNode.getLinesInDiff();
 
         return new PatternMatch<>(this,
diff --git a/src/main/java/org/variantsync/diffdetective/preliminary/pattern/semantic/AddIfdefElif.java b/src/main/java/org/variantsync/diffdetective/preliminary/pattern/semantic/AddIfdefElif.java
index f649a2c23..e1b77da16 100644
--- a/src/main/java/org/variantsync/diffdetective/preliminary/pattern/semantic/AddIfdefElif.java
+++ b/src/main/java/org/variantsync/diffdetective/preliminary/pattern/semantic/AddIfdefElif.java
@@ -10,6 +10,8 @@
 import java.util.List;
 import java.util.Optional;
 
+import static org.variantsync.diffdetective.diff.difftree.Time.AFTER;
+
 @Deprecated
 class AddIfdefElif extends SemanticPattern {
     AddIfdefElif() {
@@ -40,7 +42,7 @@ public Optional<PatternMatch<DiffNode>> match(DiffNode annotationNode) {
             }
 
             List<Node> mappings = new ArrayList<>();
-            mappings.add(annotationNode.getAfterFeatureMapping());
+            mappings.add(annotationNode.getFeatureMapping(AFTER));
             if(elifNode == null || !addedCodeInIf || !isValidElif(elifNode, mappings)){
                 return Optional.empty();
             }
@@ -68,7 +70,7 @@ private boolean isValidElif(DiffNode elifNode, List<Node> mappings) {
             }
         }
         if(addedCode && nextNode != null){
-            mappings.add(elifNode.getAfterFeatureMapping());
+            mappings.add(elifNode.getFeatureMapping(AFTER));
             return isValidElif(nextNode, mappings);
         }
 
diff --git a/src/main/java/org/variantsync/diffdetective/preliminary/pattern/semantic/AddIfdefElse.java b/src/main/java/org/variantsync/diffdetective/preliminary/pattern/semantic/AddIfdefElse.java
index e7552cb6d..ae6f93a40 100644
--- a/src/main/java/org/variantsync/diffdetective/preliminary/pattern/semantic/AddIfdefElse.java
+++ b/src/main/java/org/variantsync/diffdetective/preliminary/pattern/semantic/AddIfdefElse.java
@@ -7,6 +7,8 @@
 
 import java.util.Optional;
 
+import static org.variantsync.diffdetective.diff.difftree.Time.AFTER;
+
 @Deprecated
 class AddIfdefElse extends SemanticPattern {
     AddIfdefElse() {
@@ -55,7 +57,7 @@ public Optional<PatternMatch<DiffNode>> match(DiffNode annotationNode) {
 
             return Optional.of(new PatternMatch<>(this,
                     annotationNode.getLinesInDiff().getFromInclusive(), elseNode.getLinesInDiff().getToExclusive(),
-                    annotationNode.getAfterFeatureMapping()
+                    annotationNode.getFeatureMapping(AFTER)
             ));
         }
 
diff --git a/src/main/java/org/variantsync/diffdetective/preliminary/pattern/semantic/AddIfdefWrapElse.java b/src/main/java/org/variantsync/diffdetective/preliminary/pattern/semantic/AddIfdefWrapElse.java
index de0746ee1..b9df7a38e 100644
--- a/src/main/java/org/variantsync/diffdetective/preliminary/pattern/semantic/AddIfdefWrapElse.java
+++ b/src/main/java/org/variantsync/diffdetective/preliminary/pattern/semantic/AddIfdefWrapElse.java
@@ -6,6 +6,8 @@
 
 import java.util.Optional;
 
+import static org.variantsync.diffdetective.diff.difftree.Time.AFTER;
+
 @Deprecated
 class AddIfdefWrapElse extends SemanticPattern {
     public AddIfdefWrapElse() {
@@ -54,7 +56,7 @@ public Optional<PatternMatch<DiffNode>> match(DiffNode annotationNode) {
 
             return Optional.of(new PatternMatch<>(this,
                     annotationNode.getLinesInDiff().getFromInclusive(), elseNode.getLinesInDiff().getToExclusive(),
-                    annotationNode.getAfterFeatureMapping()
+                    annotationNode.getFeatureMapping(AFTER)
             ));
         }
 
diff --git a/src/main/java/org/variantsync/diffdetective/preliminary/pattern/semantic/AddIfdefWrapThen.java b/src/main/java/org/variantsync/diffdetective/preliminary/pattern/semantic/AddIfdefWrapThen.java
index ab673caae..3d279540d 100644
--- a/src/main/java/org/variantsync/diffdetective/preliminary/pattern/semantic/AddIfdefWrapThen.java
+++ b/src/main/java/org/variantsync/diffdetective/preliminary/pattern/semantic/AddIfdefWrapThen.java
@@ -7,6 +7,8 @@
 
 import java.util.Optional;
 
+import static org.variantsync.diffdetective.diff.difftree.Time.AFTER;
+
 @Deprecated
 class AddIfdefWrapThen extends SemanticPattern {
     AddIfdefWrapThen() {
@@ -55,7 +57,7 @@ public Optional<PatternMatch<DiffNode>> match(DiffNode annotationNode) {
 
             return Optional.of(new PatternMatch<>(this,
                     annotationNode.getLinesInDiff().getFromInclusive(), elseNode.getLinesInDiff().getToExclusive(),
-                    annotationNode.getAfterFeatureMapping()
+                    annotationNode.getFeatureMapping(AFTER)
             ));
         }
 
diff --git a/src/main/java/org/variantsync/diffdetective/preliminary/pattern/semantic/MoveElse.java b/src/main/java/org/variantsync/diffdetective/preliminary/pattern/semantic/MoveElse.java
index 7b4659b75..54bb10b00 100644
--- a/src/main/java/org/variantsync/diffdetective/preliminary/pattern/semantic/MoveElse.java
+++ b/src/main/java/org/variantsync/diffdetective/preliminary/pattern/semantic/MoveElse.java
@@ -7,6 +7,8 @@
 import java.util.Collection;
 import java.util.Optional;
 
+import static org.variantsync.diffdetective.diff.difftree.Time.AFTER;
+
 @Deprecated
 class MoveElse extends SemanticPattern {
     MoveElse() {
@@ -24,7 +26,7 @@ public Optional<PatternMatch<DiffNode>> match(DiffNode annotationNode) {
         if(annotationNode.isAdd() && annotationNode.isElse()){
 
             DiffNode removedElse = null;
-            for(DiffNode parentsChild : annotationNode.getAfterParent().getAllChildren()){
+            for(DiffNode parentsChild : annotationNode.getParent(AFTER).getAllChildren()){
                 if(parentsChild.isElse() && parentsChild.isRem()){
                     removedElse = parentsChild;
                     break;
@@ -36,10 +38,10 @@ public Optional<PatternMatch<DiffNode>> match(DiffNode annotationNode) {
             }
 
             Collection<DiffNode> commonAddElse = annotationNode.getAllChildren();
-            commonAddElse.retainAll(annotationNode.getAfterParent().getAllChildren());
+            commonAddElse.retainAll(annotationNode.getParent(AFTER).getAllChildren());
 
             Collection<DiffNode> commonRemElse = removedElse.getAllChildren();
-            commonRemElse.retainAll(annotationNode.getAfterParent().getAllChildren());
+            commonRemElse.retainAll(annotationNode.getParent(AFTER).getAllChildren());
 
             if(commonAddElse.isEmpty() && commonRemElse.isEmpty()){
                 return Optional.empty();
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/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<Integer, Pair<DiffLineNumber, DiffLineNumber>> 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();
     }

From 4fe668ef3aefbf35f9fe8d38d4b766cc4b4407c6 Mon Sep 17 00:00:00 2001
From: Benjamin Moosherr <benjamin.moosherr@uni-ulm.de>
Date: Sun, 30 Oct 2022 20:53:51 +0100
Subject: [PATCH 02/11] Use Time.forAll where appropriate

---
 .../diffdetective/diff/difftree/DiffNode.java | 39 ++++++++-----------
 .../diffdetective/diff/difftree/DiffTree.java | 18 ++++-----
 .../diffdetective/diff/difftree/Time.java     |  2 +-
 .../serialize/edgeformat/EdgeLabelFormat.java |  4 +-
 .../diff/difftree/transform/Starfold.java     |  2 +-
 5 files changed, 28 insertions(+), 37 deletions(-)

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 a6c34112d..004cdd793 100644
--- a/src/main/java/org/variantsync/diffdetective/diff/difftree/DiffNode.java
+++ b/src/main/java/org/variantsync/diffdetective/diff/difftree/DiffNode.java
@@ -286,12 +286,11 @@ public boolean addBelow(final DiffNode newBeforeParent, final DiffNode newAfterP
      * Inverse of addBelow.
      */
     public void drop() {
-        if (getParent(BEFORE) != null) {
-            getParent(BEFORE).removeChild(this, BEFORE);
-        }
-        if (getParent(AFTER) != null) {
-            getParent(AFTER).removeChild(this, AFTER);
-        }
+        Time.forAll(time -> {
+            if (getParent(time) != null) {
+                getParent(time).removeChild(this, time);
+            }
+        });
     }
 
     /**
@@ -377,8 +376,7 @@ public boolean removeChild(final DiffNode child, Time time) {
      */
     public void removeChildren(final Collection<DiffNode> childrenToRemove) {
         for (final DiffNode childToRemove : childrenToRemove) {
-            removeChild(childToRemove, BEFORE);
-            removeChild(childToRemove, AFTER);
+            Time.forAll(time -> removeChild(childToRemove, time));
         }
     }
 
@@ -426,8 +424,7 @@ private void removeFromCache(final DiffNode child) {
      * @param other The node whose children should be stolen.
      */
     public void stealChildrenOf(final DiffNode other) {
-        addChildren(other.removeChildren(BEFORE), BEFORE);
-        addChildren(other.removeChildren(AFTER), AFTER);
+        Time.forAll(time -> addChildren(other.removeChildren(time), time));
     }
 
     /**
@@ -766,12 +763,11 @@ 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.getParent(BEFORE) != null) {
-                Assert.assertTrue(c.getParent(BEFORE).isChild(c, BEFORE), () -> "The parent before the edit of " + c + " doesn't contain that node as child");
-            }
-            if (c.getParent(AFTER) != null) {
-                Assert.assertTrue(c.getParent(AFTER).isChild(c, AFTER), () -> "The parent after the edit 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
@@ -788,12 +784,11 @@ public void assertConsistency() {
 
         // Else and Elif nodes have an If or Elif as parent.
         if (this.isElse() || this.isElif()) {
-            if (getParent(BEFORE) != null) {
-                Assert.assertTrue(getParent(BEFORE).isIf() || getParent(BEFORE).isElif(), "Before parent " + getParent(BEFORE) + " of " + this + " is neither IF nor ELIF!");
-            }
-            if (getParent(AFTER) != null) {
-                Assert.assertTrue(getParent(AFTER).isIf() || getParent(AFTER).isElif(), "After parent " + getParent(AFTER) + " 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
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 a6123548f..d9e0b47b9 100644
--- a/src/main/java/org/variantsync/diffdetective/diff/difftree/DiffTree.java
+++ b/src/main/java/org/variantsync/diffdetective/diff/difftree/DiffTree.java
@@ -340,17 +340,13 @@ public boolean isEmpty() {
     public void removeNode(DiffNode node) {
         Assert.assertTrue(node != root);
 
-        final DiffNode beforeParent = node.getParent(BEFORE);
-        if (beforeParent != null) {
-            beforeParent.removeChild(node, BEFORE);
-            beforeParent.addChildren(node.removeChildren(BEFORE), BEFORE);
-        }
-
-        final DiffNode afterParent = node.getParent(AFTER);
-        if (afterParent != null) {
-            afterParent.removeChild(node, AFTER);
-            afterParent.addChildren(node.removeChildren(AFTER), AFTER);
-        }
+        Time.forAll(time -> {
+            final DiffNode parent = node.getParent(time);
+            if (parent != null) {
+                parent.removeChild(node, time);
+                parent.addChildren(node.removeChildren(time), time);
+            }
+        });
     }
 
     /**
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 df55931f1..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<Time> f) {
+    public static void forAll(final Consumer<Time> f) {
         f.accept(BEFORE);
         f.accept(AFTER);
     }
diff --git a/src/main/java/org/variantsync/diffdetective/diff/difftree/serialize/edgeformat/EdgeLabelFormat.java b/src/main/java/org/variantsync/diffdetective/diff/difftree/serialize/edgeformat/EdgeLabelFormat.java
index 5b9c10925..fbcc173be 100644
--- a/src/main/java/org/variantsync/diffdetective/diff/difftree/serialize/edgeformat/EdgeLabelFormat.java
+++ b/src/main/java/org/variantsync/diffdetective/diff/difftree/serialize/edgeformat/EdgeLabelFormat.java
@@ -2,6 +2,7 @@
 
 import org.variantsync.diffdetective.diff.difftree.DiffNode;
 import org.variantsync.diffdetective.diff.difftree.LineGraphConstants;
+import org.variantsync.diffdetective.diff.difftree.Time;
 import org.variantsync.diffdetective.diff.difftree.serialize.StyledEdge;
 import org.variantsync.diffdetective.diff.difftree.serialize.LinegraphFormat;
 import org.variantsync.diffdetective.util.Assert;
@@ -98,8 +99,7 @@ public Direction getEdgeDirection() {
     protected void connectAccordingToLabel(final DiffNode child, final DiffNode parent, final String edgeLabel) {
         if (edgeLabel.startsWith(LineGraphConstants.BEFORE_AND_AFTER_PARENT)) {
             // Nothing has been changed. The child-parent relationship remains the same
-            Assert.assertTrue(parent.addChild(child, AFTER));
-            Assert.assertTrue(parent.addChild(child, BEFORE));
+            Time.forAll(time -> Assert.assertTrue(parent.addChild(child, time)));
         } else if (edgeLabel.startsWith(LineGraphConstants.BEFORE_PARENT)) {
             // The child DiffNode lost its parent DiffNode (an orphan DiffNode)
             Assert.assertTrue(parent.addChild(child, BEFORE));
diff --git a/src/main/java/org/variantsync/diffdetective/diff/difftree/transform/Starfold.java b/src/main/java/org/variantsync/diffdetective/diff/difftree/transform/Starfold.java
index 3ab175db1..0ea549e26 100644
--- a/src/main/java/org/variantsync/diffdetective/diff/difftree/transform/Starfold.java
+++ b/src/main/java/org/variantsync/diffdetective/diff/difftree/transform/Starfold.java
@@ -56,7 +56,7 @@ public void transform(DiffTree diffTree) {
 
     private void foldStar(final DiffNode starRoot) {
         // We fold the stars for each time respectively.
-        Time.forall(t -> foldStarAtTime(starRoot, t));
+        Time.forAll(t -> foldStarAtTime(starRoot, t));
     }
 
     private void foldStarAtTime(final DiffNode starRoot, Time time) {

From b4eee4d565d40dfd34c4802d1c71269f16307ae0 Mon Sep 17 00:00:00 2001
From: Benjamin Moosherr <benjamin.moosherr@uni-ulm.de>
Date: Fri, 4 Nov 2022 13:44:08 +0100
Subject: [PATCH 03/11] Factor `NodeType` related functions into a mixin

---
 .../diffdetective/diff/difftree/DiffNode.java | 43 ++---------------
 .../variationtree/HasNodeType.java            | 46 +++++++++++++++++++
 2 files changed, 51 insertions(+), 38 deletions(-)
 create mode 100644 src/main/java/org/variantsync/diffdetective/variationtree/HasNodeType.java

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 004cdd793..fd779bc9b 100644
--- a/src/main/java/org/variantsync/diffdetective/diff/difftree/DiffNode.java
+++ b/src/main/java/org/variantsync/diffdetective/diff/difftree/DiffNode.java
@@ -7,6 +7,7 @@
 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 java.util.*;
 import java.util.stream.Collectors;
@@ -24,7 +25,7 @@
  * DiffNode's store parent and child information to build a graph.
  * @author Paul Bittner, Sören Viegener, Benjamin Moosherr
  */
-public class DiffNode {
+public class DiffNode implements HasNodeType {
     private static final short ID_OFFSET = 3;
 
     /**
@@ -657,36 +658,9 @@ 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;
     }
 
     /**
@@ -696,13 +670,6 @@ public boolean isRoot() {
         return getParent(BEFORE) == null && getParent(AFTER) == null;
     }
 
-    /**
-     * Returns {@link NodeType#isAnnotation()} for this node's {@link DiffNode#nodeType}.
-     */
-    public boolean isAnnotation() {
-        return this.nodeType.isAnnotation();
-    }
-
     /**
      * @return An integer that uniquely identifiers this DiffNode within its patch.
      *
diff --git a/src/main/java/org/variantsync/diffdetective/variationtree/HasNodeType.java b/src/main/java/org/variantsync/diffdetective/variationtree/HasNodeType.java
new file mode 100644
index 000000000..12358bd00
--- /dev/null
+++ b/src/main/java/org/variantsync/diffdetective/variationtree/HasNodeType.java
@@ -0,0 +1,46 @@
+package org.variantsync.diffdetective.variationtree;
+
+import org.variantsync.diffdetective.diff.difftree.NodeType;
+
+/**
+ * Mixin providing convenience methods for checking certain node types.
+ *
+ * @author Benjamin Moosherr
+ */
+public interface HasNodeType {
+    /** Returns the type of this node. */
+    public NodeType getNodeType();
+
+    /** Returns {@code true} iff this node is an {@link NodeType#ARTIFACT artifact}. */
+    public default boolean isArtifact() {
+        return getNodeType().equals(NodeType.ARTIFACT);
+    }
+
+    /** Returns {@code true} iff this node is an {@link NodeType#IF if}. */
+    public default boolean isIf() {
+        return getNodeType().equals(NodeType.IF);
+    }
+
+    /** Returns {@code true} iff this node is an {@link NodeType#ELIF elif}. */
+    public default boolean isElif() {
+        return getNodeType().equals(NodeType.ELIF);
+    }
+
+    /** Returns {@code true} iff this node is an {@link NodeType#ELSE else}. */
+    public default boolean isElse() {
+        return getNodeType().equals(NodeType.ELSE);
+    }
+
+    /** Returns {@code true} iff this node is an {@link NodeType#isAnnotation() annotation}. */
+    public default boolean isAnnotation() {
+        return getNodeType().isAnnotation();
+    }
+
+    /**
+     * Returns {@code true} iff this node is a {@link NodeType#isConditionalAnnotation()
+     * conditional annotation}.
+     */
+    public default boolean isConditionalAnnotation() {
+        return getNodeType().isConditionalAnnotation();
+    }
+}

From c76d5974ca20fb0850be63ffd995338c023e3624 Mon Sep 17 00:00:00 2001
From: Benjamin Moosherr <benjamin.moosherr@uni-ulm.de>
Date: Tue, 8 Nov 2022 16:58:04 +0100
Subject: [PATCH 04/11] Update Functjonal to get `FilteredMappedListView`

---
 lib/functjonal-1.0-SNAPSHOT.jar               | Bin 44965 -> 48696 bytes
 .../functjonal-1.0-20220705.154223-1.jar.md5  |   1 -
 .../functjonal-1.0-20220705.154223-1.jar.sha1 |   1 -
 ...r => functjonal-1.0-20221108.155446-1.jar} | Bin 44965 -> 48696 bytes
 .../functjonal-1.0-20221108.155446-1.jar.md5  |   1 +
 .../functjonal-1.0-20221108.155446-1.jar.sha1 |   1 +
 ...m => functjonal-1.0-20221108.155446-1.pom} |   0
 ... functjonal-1.0-20221108.155446-1.pom.md5} |   0
 ...functjonal-1.0-20221108.155446-1.pom.sha1} |   0
 .../1.0-SNAPSHOT/maven-metadata.xml           |  14 +++++++-------
 .../1.0-SNAPSHOT/maven-metadata.xml.md5       |   2 +-
 .../1.0-SNAPSHOT/maven-metadata.xml.sha1      |   2 +-
 .../variantsync/functjonal/maven-metadata.xml |   2 +-
 .../functjonal/maven-metadata.xml.md5         |   2 +-
 .../functjonal/maven-metadata.xml.sha1        |   2 +-
 15 files changed, 14 insertions(+), 14 deletions(-)
 delete mode 100644 local-maven-repo/org/variantsync/functjonal/1.0-SNAPSHOT/functjonal-1.0-20220705.154223-1.jar.md5
 delete mode 100644 local-maven-repo/org/variantsync/functjonal/1.0-SNAPSHOT/functjonal-1.0-20220705.154223-1.jar.sha1
 rename local-maven-repo/org/variantsync/functjonal/1.0-SNAPSHOT/{functjonal-1.0-20220705.154223-1.jar => functjonal-1.0-20221108.155446-1.jar} (64%)
 create mode 100644 local-maven-repo/org/variantsync/functjonal/1.0-SNAPSHOT/functjonal-1.0-20221108.155446-1.jar.md5
 create mode 100644 local-maven-repo/org/variantsync/functjonal/1.0-SNAPSHOT/functjonal-1.0-20221108.155446-1.jar.sha1
 rename local-maven-repo/org/variantsync/functjonal/1.0-SNAPSHOT/{functjonal-1.0-20220705.154223-1.pom => functjonal-1.0-20221108.155446-1.pom} (100%)
 rename local-maven-repo/org/variantsync/functjonal/1.0-SNAPSHOT/{functjonal-1.0-20220705.154223-1.pom.md5 => functjonal-1.0-20221108.155446-1.pom.md5} (100%)
 rename local-maven-repo/org/variantsync/functjonal/1.0-SNAPSHOT/{functjonal-1.0-20220705.154223-1.pom.sha1 => functjonal-1.0-20221108.155446-1.pom.sha1} (100%)

diff --git a/lib/functjonal-1.0-SNAPSHOT.jar b/lib/functjonal-1.0-SNAPSHOT.jar
index 654612e33bbb0d94e32ed8f3832e50e75b0e1c3e..7e3171051000675dfdcb18f06db31634eab31fa7 100644
GIT binary patch
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 654612e33bbb0d94e32ed8f3832e50e75b0e1c3e..7e3171051000675dfdcb18f06db31634eab31fa7 100644
GIT binary patch
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 @@
 <metadata modelVersion="1.1.0">
   <groupId>org.variantsync</groupId>
   <artifactId>functjonal</artifactId>
-  <version>1.0-SNAPSHOT</version>
   <versioning>
+    <lastUpdated>20221108155446</lastUpdated>
     <snapshot>
-      <timestamp>20220705.154223</timestamp>
+      <timestamp>20221108.155446</timestamp>
       <buildNumber>1</buildNumber>
     </snapshot>
-    <lastUpdated>20220705154223</lastUpdated>
     <snapshotVersions>
       <snapshotVersion>
         <extension>jar</extension>
-        <value>1.0-20220705.154223-1</value>
-        <updated>20220705154223</updated>
+        <value>1.0-20221108.155446-1</value>
+        <updated>20221108155446</updated>
       </snapshotVersion>
       <snapshotVersion>
         <extension>pom</extension>
-        <value>1.0-20220705.154223-1</value>
-        <updated>20220705154223</updated>
+        <value>1.0-20221108.155446-1</value>
+        <updated>20221108155446</updated>
       </snapshotVersion>
     </snapshotVersions>
   </versioning>
+  <version>1.0-SNAPSHOT</version>
 </metadata>
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 @@
     <versions>
       <version>1.0-SNAPSHOT</version>
     </versions>
-    <lastUpdated>20220705154223</lastUpdated>
+    <lastUpdated>20221108155446</lastUpdated>
   </versioning>
 </metadata>
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

From 92f170ebe4832ac3e1bf66aab988fbfd447b056f Mon Sep 17 00:00:00 2001
From: Benjamin Moosherr <benjamin.moosherr@uni-ulm.de>
Date: Fri, 4 Nov 2022 13:43:25 +0100
Subject: [PATCH 05/11] Implement variation trees and variation tree
 projections

---
 .../diffdetective/diff/difftree/DiffNode.java |  67 +-
 .../diff/difftree/Projection.java             | 188 ++++++
 .../serialize/nodeformat/FullNodeFormat.java  |   2 +-
 .../diff/difftree/transform/Starfold.java     |   2 +-
 .../variationtree/VariationNode.java          | 599 ++++++++++++++++++
 .../variationtree/VariationTree.java          | 122 ++++
 .../variationtree/VariationTreeNode.java      | 364 +++++++++++
 .../variationtree/source/GitSource.java       |  27 +
 .../variationtree/source/LocalFileSource.java |  13 +
 .../source/VariationTreeSource.java           |  19 +
 10 files changed, 1396 insertions(+), 7 deletions(-)
 create mode 100644 src/main/java/org/variantsync/diffdetective/diff/difftree/Projection.java
 create mode 100644 src/main/java/org/variantsync/diffdetective/variationtree/VariationNode.java
 create mode 100644 src/main/java/org/variantsync/diffdetective/variationtree/VariationTree.java
 create mode 100644 src/main/java/org/variantsync/diffdetective/variationtree/VariationTreeNode.java
 create mode 100644 src/main/java/org/variantsync/diffdetective/variationtree/source/GitSource.java
 create mode 100644 src/main/java/org/variantsync/diffdetective/variationtree/source/LocalFileSource.java
 create mode 100644 src/main/java/org/variantsync/diffdetective/variationtree/source/VariationTreeSource.java

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 fd779bc9b..eb2ed8e82 100644
--- a/src/main/java/org/variantsync/diffdetective/diff/difftree/DiffNode.java
+++ b/src/main/java/org/variantsync/diffdetective/diff/difftree/DiffNode.java
@@ -8,6 +8,7 @@
 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.stream.Collectors;
@@ -66,6 +67,16 @@ public class DiffNode implements HasNodeType {
      */
     private final List<DiffNode> 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()}
+     *
+     * <p>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.
@@ -141,14 +152,15 @@ public void addLines(final List<String> lines) {
 
     /**
      * Returns the lines in the diff that are represented by this DiffNode.
+     * The returned list is unmodifiable.
      */
-    public List<String> getLines() {
-        return lines;
+    public List<String> 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);
@@ -295,7 +307,7 @@ public void drop() {
     }
 
     /**
-     * Remove this as the parent of {@code child}.
+     * 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);
@@ -383,7 +395,7 @@ public void removeChildren(final Collection<DiffNode> childrenToRemove) {
 
     /**
      * Removes all children before or after the edit.
-     * Afterwards, this node will have no before children.
+     * 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.
      */
@@ -814,6 +826,51 @@ public String toTextDiff() {
         return diff.toString();
     }
 
+    /**
+     * Returns a view of this {@code DiffNode} as a variation node at the time {@code time}.
+     *
+     * <p>See the {@code project} function in section 3.1 of
+     * <a href="https://github.com/SoftVarE-Group/Papers/raw/main/2022/2022-ESECFSE-Bittner.pdf">
+     * our paper</a>.
+     */
+    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 <T> DiffNode unchanged(VariationNode<T> variationNode) {
+        int from = variationNode.getFromLine();
+        int to = variationNode.getToLine();
+
+        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/Projection.java b/src/main/java/org/variantsync/diffdetective/diff/difftree/Projection.java
new file mode 100644
index 000000000..07202d88f
--- /dev/null
+++ b/src/main/java/org/variantsync/diffdetective/diff/difftree/Projection.java
@@ -0,0 +1,188 @@
+package org.variantsync.diffdetective.diff.difftree;
+
+import java.util.List;
+import java.util.Optional;
+
+import org.prop4j.Node;
+import org.variantsync.diffdetective.diff.DiffLineNumber;
+import org.variantsync.diffdetective.variationtree.VariationNode;
+import org.variantsync.functjonal.list.FilteredMappedListView;
+
+import static org.variantsync.diffdetective.diff.difftree.Time.AFTER;
+import static org.variantsync.diffdetective.diff.difftree.Time.BEFORE;
+
+/**
+ * A view of a {@link DiffNode} as variation node at a specific time.
+ *
+ * <p>See the {@code project} function in section 3.1 of
+ * <a href="https://github.com/SoftVarE-Group/Papers/raw/main/2022/2022-ESECFSE-Bittner.pdf">
+ * our paper</a>.
+ *
+ * <p>This class has to be instantiated using {@link DiffNode#projection}.
+ *
+ * <p>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<DiffNode> {
+    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;
+    }
+
+    @Override
+    public DiffNode getBackingNode() {
+        return backingNode;
+    }
+
+    @Override
+    public NodeType getNodeType() {
+        return getBackingNode().nodeType;
+    }
+
+    @Override
+    public List<String> getLabelLines() {
+        return getBackingNode().getLabelLines();
+    }
+
+    @Override
+    public int getFromLine() {
+        return getBackingNode().getFromLine().atTime(time);
+    }
+
+    @Override
+    public void setFromLine(int from) {
+        var node = getBackingNode();
+        var current = node.getFromLine();
+        node.setFromLine(new DiffLineNumber(
+            current.inDiff(),
+            time == BEFORE ? from : current.beforeEdit(),
+            time == AFTER ? from : current.afterEdit()
+        ));
+    }
+
+    @Override
+    public int getToLine() {
+        return getBackingNode().getToLine().atTime(time);
+    }
+
+    @Override
+    public void setToLine(int to) {
+        var node = getBackingNode();
+        var current = node.getToLine();
+        node.setToLine(new DiffLineNumber(
+            current.inDiff(),
+            time == BEFORE ? to : current.beforeEdit(),
+            time == AFTER ? to : current.afterEdit()
+        ));
+    }
+
+    @Override
+    public VariationNode<DiffNode> getParent() {
+        var parent = getBackingNode().getParent(time);
+
+        if (parent == null) {
+            return null;
+        } else {
+            return parent.projection(time);
+        }
+    }
+
+
+    @Override
+    public List<VariationNode<DiffNode>> 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 VariationNode<DiffNode> child) {
+        getBackingNode().addChild(child.getBackingNode(), time);
+    }
+
+    @Override
+    public void insertChild(final VariationNode<DiffNode> 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 VariationNode<DiffNode> 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(VariationNode<DiffNode> 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/serialize/nodeformat/FullNodeFormat.java b/src/main/java/org/variantsync/diffdetective/diff/difftree/serialize/nodeformat/FullNodeFormat.java
index c8e4ee82e..75b4c81db 100644
--- a/src/main/java/org/variantsync/diffdetective/diff/difftree/serialize/nodeformat/FullNodeFormat.java
+++ b/src/main/java/org/variantsync/diffdetective/diff/difftree/serialize/nodeformat/FullNodeFormat.java
@@ -31,7 +31,7 @@ public List<String> toMultilineLabel(final DiffNode node) {
         lines.add(node.getFromLine().toString());
         lines.add(node.getToLine().toString());
         lines.add(node.getDirectFeatureMapping() == null ? "" : node.getDirectFeatureMapping().toString());
-        lines.addAll(node.getLines());
+        lines.addAll(node.getLabelLines());
 
         return lines;
     }
diff --git a/src/main/java/org/variantsync/diffdetective/diff/difftree/transform/Starfold.java b/src/main/java/org/variantsync/diffdetective/diff/difftree/transform/Starfold.java
index 0ea549e26..dc3ad5a00 100644
--- a/src/main/java/org/variantsync/diffdetective/diff/difftree/transform/Starfold.java
+++ b/src/main/java/org/variantsync/diffdetective/diff/difftree/transform/Starfold.java
@@ -94,7 +94,7 @@ private void mergeArms(final DiffNode starRoot, Time time, final DiffType target
                             targetDiffType,
                             DiffLineNumber.Invalid(),
                             DiffLineNumber.Invalid(),
-                            starArms.stream().flatMap(node -> node.getLines().stream()).toList()
+                            starArms.stream().flatMap(node -> node.getLabelLines().stream()).toList()
                     ),
                     targetIndex,
                     time
diff --git a/src/main/java/org/variantsync/diffdetective/variationtree/VariationNode.java b/src/main/java/org/variantsync/diffdetective/variationtree/VariationNode.java
new file mode 100644
index 000000000..344751a24
--- /dev/null
+++ b/src/main/java/org/variantsync/diffdetective/variationtree/VariationNode.java
@@ -0,0 +1,599 @@
+package org.variantsync.diffdetective.variationtree;
+
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.function.Consumer;
+
+import org.prop4j.And;
+import org.prop4j.Node;
+import org.variantsync.diffdetective.diff.Lines;
+import org.variantsync.diffdetective.diff.difftree.DiffNode; // For Javadoc
+import org.variantsync.diffdetective.diff.difftree.NodeType;
+import org.variantsync.diffdetective.diff.difftree.Projection; // For Javadoc
+import org.variantsync.diffdetective.util.Assert;
+import org.variantsync.diffdetective.util.fide.FixTrueFalse;
+
+import static org.variantsync.diffdetective.util.fide.FormulaUtils.negate;
+
+/**
+ * A base class for views of a node in a variation tree.
+ *
+ * <p>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.
+ *
+ * <p>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)}).
+ *
+ * <p>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 <T> the backing type storing the actual information presented by this class. This is
+ * currently either a {@link VariationTreeNode concrete variation tree} (in this case
+ * {@link getBackingNode} acts as identity) or a {@link Projection} of a {@link DiffNode}.
+ *
+ * @see assertConsistency
+ * @author Benjamin Moosherr
+ */
+public abstract class VariationNode<T> implements HasNodeType {
+    /**
+     * Returns the class instance storing the actual attributes of this node. This currently is
+     * either a {@link VariationTreeNode concrete variation tree} or the projection of a
+     * {@link DiffNode} at a specific time.
+     */
+    public abstract T getBackingNode();
+
+    /**
+     * 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.
+     *
+     * <p>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<String> getLabelLines();
+
+    /**
+     * Returns the start line number of the {@link getLabelLines label} of this node in the
+     * corresponding source code.
+     *
+     * @see setFromLine
+     */
+    public abstract int getFromLine();
+
+    /**
+     * Changes the line number returned by {@link getFromLine}.
+     *
+     * @see getFromLine
+     */
+    public abstract void setFromLine(int from);
+
+    /**
+     * Returns the end line number of the {@link getLabelLines label} of this node in the
+     * corresponding source code.
+     *
+     * <p>The line number is exclusive (i.e., it points 1 behind the last included line).
+     *
+     * @see setToLine
+     */
+    public abstract int getToLine();
+
+    /**
+     * Changes the line number returned by {@link getToLine}.
+     *
+     * @see getToLine
+     */
+    public abstract void setToLine(int to);
+
+    /**
+     * Returns the range of line numbers of this node's corresponding source code.
+     *
+     * @see getFromLine
+     * @see getToLine
+     */
+    public Lines getLineRange() {
+        return Lines.FromInclToExcl(getFromLine(), getToLine());
+    }
+
+    /**
+     * Returns the parent of this node, or {@code null} if this node doesn't have a parent.
+     *
+     * @see drop
+     * @see addBelow
+     */
+    public abstract VariationNode<T> getParent();
+
+    /**
+     * Returns an unmodifiable list representing the children of this node.
+     *
+     * <p>The following invariant has to hold for all {@code node}s:
+     * <code>
+     *   for (var child : node.getChildren()) {
+     *     Assert.assertTrue(node.isSameAs(child.getParent(node)))
+     *   }
+     * </code>
+     *
+     * @see isChild
+     * @see addChild
+     * @see removeChild
+     */
+    public abstract List<VariationNode<T>> 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.
+     *
+     * <p>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;
+    }
+
+    /**
+     * Computes the number of if-elif-else chains of the path from the root node to this node.
+     */
+    public int getAnnotationDepth() {
+        if (isRoot()) {
+            return 0;
+        }
+
+        if (isIf()) {
+            return getParent().getAnnotationDepth() + 1;
+        }
+
+        return getParent().getAnnotationDepth();
+    }
+
+    /**
+     * Returns the first {@code if} node in the path from this node upwards to the root.
+     */
+    public VariationNode<T> getIfNode() {
+        if (isIf()) {
+            return this;
+        }
+        if (isRoot()) {
+            return null;
+        }
+        return getParent().getIfNode();
+    }
+
+    /**
+     * Returns {@code true} iff this node is the parent of the given node.
+     *
+     * @see getChildren
+     */
+    public boolean isChild(VariationNode<T> child) {
+        return child.getParent().isSameAs(this);
+    }
+
+    /**
+     * 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.
+     *
+     * <p>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 VariationNode<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 VariationNode<T> child);
+
+    /**
+     * Adds a child before the given index to the children list of this node and sets its parent to
+     * this node.
+     *
+     * <p>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 VariationNode<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<VariationNode<T>> 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 VariationNode<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<VariationNode<T>> 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 VariationNode<T> parent) {
+        if (parent != null) {
+            parent.addChild(this);
+        }
+    }
+
+    /**
+     * 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);
+        }
+    }
+
+    /**
+     * 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 VariationNode<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}).
+     *
+     * <p>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<Node> getFeatureMappingClauses() {
+        final var parent = getParent();
+
+        if (isElse() || isElif()) {
+            List<Node> 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.getFeatureMappingClauses();
+        }
+
+        return List.of(getDirectFeatureMapping());
+    }
+
+    /**
+     * Returns the full feature mapping formula of this node.
+     *
+     * <p>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
+     * <a href="https://github.com/SoftVarE-Group/Papers/raw/main/2022/2022-ESECFSE-Bittner.pdf">
+     * our paper</a>.
+     *
+     * @return the feature mapping of this node
+     */
+    public Node getFeatureMapping() {
+        final List<Node> 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<Node> getPresenceConditionClauses() {
+        final var parent = getParent();
+
+        if (isElse() || isElif()) {
+            final List<Node> 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.getPresenceConditionClauses());
+            }
+
+            return clauses;
+        } else if (isArtifact()) {
+            return parent.getPresenceConditionClauses();
+        }
+
+        // this is mapping or root
+        final List<Node> clauses;
+        if (parent == null) {
+            clauses = new ArrayList<>(1);
+        } else {
+            clauses = parent.getPresenceConditionClauses();
+        }
+        clauses.add(getDirectFeatureMapping());
+        return clauses;
+    }
+
+    /**
+     * Returns the presence condition of this node.
+     * See Equation (2) in
+     * <a href="https://github.com/SoftVarE-Group/Papers/raw/main/2022/2022-ESECFSE-Bittner.pdf">
+     * our paper</a>.
+     */
+    public Node getPresenceCondition() {
+        final List<Node> 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<VariationNode<T>> action) {
+        action.accept(this);
+
+        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(),
+            getFromLine(),
+            getToLine(),
+            getDirectFeatureMapping().clone(),
+            new ArrayList<String>(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
+    * <ul>
+    * <li>if-chains are nested correctly,
+    * <li>the root is an {@link NodeType#IF} with the feature mapping {@code "true"},
+    * <li>the feature mapping is {@code null} iff {@code isConditionalAnnotation} is {@code false}
+    * and
+    * <li>all edges are well-formed (e.g., edges can be inconsistent because edges are
+    * double-linked).
+    * </ul>
+    *
+    * <p>Some invariants are not checked. These include
+    * <ul>
+    * <li>There should be no cycles and
+    * <li>{@link getID} should be unique in the whole variation tree.
+    * </ul>
+    *
+    * @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), () ->
+                "The parent (" + this + ") of " + child + " is not set correctly");
+        }
+    }
+
+
+
+    /**
+     * Returns an integer that uniquely identifies this node within its tree.
+     *
+     * <p>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 getFromLine}. Beware that {@link Projection} returns {@link DiffNode#getID} so this
+     * id is not compatible with {@link VariationTreeNode#getID}.
+     */
+    public abstract int getID();
+
+    /**
+     * Checks if {@code other} represents the same node as this node.
+     *
+     * <p>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
+     * {@link getBackingNode backing nodes} and {@link equals} does not.
+     *
+     * <p>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(VariationNode<T> other);
+
+    @Override
+    public String toString() {
+        return getBackingNode().toString();
+    }
+
+    /**
+     * Unparses the labels of this subtree into {@code output}.
+     *
+     * <p>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..a2f170df2
--- /dev/null
+++ b/src/main/java/org/variantsync/diffdetective/variationtree/VariationTreeNode.java
@@ -0,0 +1,364 @@
+package org.variantsync.diffdetective.variationtree;
+
+import org.prop4j.Node;
+import org.variantsync.diffdetective.diff.DiffLineNumber;
+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.
+ *
+ * <p>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}.
+ *
+ * <p>See definition 2.2 of
+ * <a href="https://github.com/SoftVarE-Group/Papers/raw/main/2022/2022-ESECFSE-Bittner.pdf">
+ * our ESEC/FSE'22 paper</a> 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.
+ *
+ * <p>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.
+ *
+ * <p>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.
+ *
+ * <p>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<VariationTreeNode> {
+    private static final short ID_OFFSET = 3;
+
+    /**
+     * The node type of this node, which determines the type of the represented
+     * element in the diff (e.g., mapping or artifact).
+     */
+    public final NodeType nodeType;
+
+    /**
+     * The start line number of the {@link label} of this node in the corresponding source code.
+     */
+    private int from = DiffLineNumber.InvalidLineNumber;
+    /**
+     * The end line number of the {@link label} of this node in the corresponding source code.
+     * The line number is exclusive (i.e., it points 1 behind the last included line).
+     */
+    private int to = DiffLineNumber.InvalidLineNumber;
+
+    /**
+     * A list of lines representing the label of this node.
+     */
+    private List<String> label;
+
+    /**
+     * The direct feature mapping of this node.
+     *
+     * <p>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.
+     *
+     * <p>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<VariationTreeNode> 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 fromLine the start line number of the code of {@code label}
+     * @param toLine the end line number (exclusive) of the code of {@code label}
+     * @param featureMapping the direct feature mapping of this node, has be non null iff
+     * {@code nodeType.isConditionalAnnotation} is {@code true}
+     * @param label a list of lines used as label
+     * @see addChild
+     * @see addBelow
+     */
+    public VariationTreeNode(
+        NodeType nodeType,
+        int fromLine,
+        int toLine,
+        Node featureMapping,
+        List<String> label
+    ) {
+        super();
+
+        this.nodeType = nodeType;
+        this.from = fromLine;
+        this.to = toLine;
+        this.label = label;
+        this.featureMapping = featureMapping;
+
+        this.childOrder = new ArrayList<>();
+    }
+
+    /**
+     * Convenience constructor for creating the root of a {@link VariationTree}.
+     *
+     * <p>The newly created root has no children yet.
+     *
+     * <p>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,
+                DiffLineNumber.InvalidLineNumber,
+                DiffLineNumber.InvalidLineNumber,
+                FixTrueFalse.True,
+                new ArrayList<>()
+        );
+    }
+
+    /**
+     * Convenience constructor for creating an artifact node of a {@link VariationTree}.
+     *
+     * @param fromLine the start line number of the code of {@code label}
+     * @param toLine the end line number (exclusive) of the code of {@code label}
+     * @param label a list of lines used as label
+     * @see addBelow
+     */
+    public static VariationTreeNode createArtifact(int fromLine, int toLine, List<String> label) {
+        return new VariationTreeNode(NodeType.ARTIFACT, fromLine, toLine, null, label);
+    }
+
+    @Override
+    public VariationTreeNode getBackingNode() {
+        return this;
+    }
+
+    @Override
+    public NodeType getNodeType() {
+        return nodeType;
+    }
+
+    @Override
+    public List<String> getLabelLines() {
+        return Collections.unmodifiableList(label);
+    }
+
+    /**
+     * Replaces the current label by {@code newLabelLines}.
+     *
+     * <p>The given list is not copied, so modifications of {@code newLabelLines} will be visible by
+     * {@link getLabelLines}.
+     *
+     * @see getLabelLines
+     */
+    public void setLabelLines(List<String> 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<String> lines) {
+        this.label.addAll(lines);
+    }
+
+    @Override
+    public int getFromLine() {
+        return from;
+    }
+
+    @Override
+    public void setFromLine(int from) {
+        this.from = from;
+    }
+
+    @Override
+    public int getToLine() {
+        return to;
+    }
+
+    @Override
+    public void setToLine(int to) {
+        this.to = to;
+    }
+
+    @Override
+    public VariationNode<VariationTreeNode> getParent() {
+        return parent;
+    }
+
+    /**
+     * Sets {@code newParent} as the new parent of this node.
+     *
+     * <p>This node must not have a parent yet.
+     */
+    private void setParent(final VariationTreeNode newParent) {
+        Assert.assertTrue(parent == null);
+        this.parent = newParent;
+    }
+
+    @Override
+    public List<VariationNode<VariationTreeNode>> getChildren() {
+        return Collections.unmodifiableList(childOrder);
+    }
+
+    @Override
+    public void addChild(final VariationNode<VariationTreeNode> child) {
+        child.getBackingNode().setParent(this);
+        childOrder.add(child.getBackingNode());
+    }
+
+    @Override
+    public void insertChild(final VariationNode<VariationTreeNode> child, int index) {
+        child.getBackingNode().setParent(this);
+        childOrder.add(index, child.getBackingNode());
+    }
+
+    @Override
+    public boolean removeChild(final VariationNode<VariationTreeNode> child) {
+        if (isChild(child)) {
+            child.getBackingNode().parent = null;
+            childOrder.remove(child.getBackingNode());
+            return true;
+        }
+        return false;
+    }
+
+    @Override
+    public void removeAllChildren() {
+        for (var child : childOrder) {
+            child.getBackingNode().parent = null;
+        }
+
+        childOrder.clear();
+    }
+
+    @Override
+    public Node getDirectFeatureMapping() {
+        return featureMapping;
+    }
+
+    /**
+     * Returns an integer that uniquely identifiers this node within its variation tree.
+     *
+     * <p>From the returned id a new node with all essential attributes ({@link getNodeType node
+     * type} and {@link getFromLine start line number}) can be reconstructed by using
+     * {@link fromID}.
+     *
+     * <p>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.
+        int lineNumber = 1 + from;
+        Assert.assertTrue((lineNumber << 2*ID_OFFSET) >> 2*ID_OFFSET == lineNumber);
+
+        int id;
+        id = lineNumber;
+        id <<= ID_OFFSET;
+        // This makes `VariationTreeNode.toID` compatible with `DiffNode.toID`
+        id += DiffType.NON.ordinal();
+        id <<= ID_OFFSET;
+        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(final int id, List<String> label) {
+        final int lowestBitsMask = (1 << ID_OFFSET) - 1;
+
+        final int nodeTypeOrdinal = id & lowestBitsMask;
+        Assert.assertTrue(((id >> ID_OFFSET) & lowestBitsMask) == DiffType.NON.ordinal());
+        final int from = (id >> (2*ID_OFFSET)) - 1;
+
+        var nodeType = NodeType.values()[nodeTypeOrdinal];
+        return new VariationTreeNode(
+                nodeType,
+                from,
+                DiffLineNumber.InvalidLineNumber,
+                nodeType.isConditionalAnnotation() ? FixTrueFalse.True : null,
+                label
+        );
+    }
+
+    @Override
+    public boolean isSameAs(VariationNode<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 && from == other.from && to == other.to && Objects.equals(featureMapping, other.featureMapping) && label.equals(other.label);
+    }
+
+    /**
+     * Compute a hash using all available attributes.
+     *
+     * <p>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, from, to, featureMapping, label);
+    }
+
+    @Override
+    public String toString() {
+        String s;
+        if (isArtifact()) {
+            s = String.format("%s from %d to %d", nodeType, from, to);
+        } else if (isRoot()) {
+            s = "ROOT";
+        } else {
+            s = String.format("%s from %d to %d with \"%s\"", nodeType,
+                    from, to, 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.
+ *
+ * <p>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:
+ * <code>
+ *   git clone "$repository" repository
+ *   cd repository
+ *   git switch -d "$commitHash"
+ *   cat "$path"
+ * </code>
+ */
+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();
+}

From c4c6b290d5a6a3c2d91f2b1f113151faff647c9b Mon Sep 17 00:00:00 2001
From: Benjamin Moosherr <benjamin.moosherr@uni-ulm.de>
Date: Sat, 5 Nov 2022 18:24:37 +0100
Subject: [PATCH 06/11] Remove code duplication cause by `DiffNode.projection`

---
 .../diffdetective/diff/difftree/DiffNode.java | 116 +-----------------
 1 file changed, 6 insertions(+), 110 deletions(-)

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 eb2ed8e82..01add235c 100644
--- a/src/main/java/org/variantsync/diffdetective/diff/difftree/DiffNode.java
+++ b/src/main/java/org/variantsync/diffdetective/diff/difftree/DiffNode.java
@@ -1,6 +1,5 @@
 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;
@@ -15,7 +14,6 @@
 
 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;
 
 /**
  * Implementation of a node in a {@link DiffTree}.
@@ -181,13 +179,7 @@ public void setLabel(String label) {
      * @return The first {@code if} node in the path to the root at the time {@code time}
      */
     public DiffNode getIfNode(Time time) {
-        if (isIf()) {
-            return this;
-        }
-        if (isRoot()) {
-            return null;
-        }
-        return getParent(time).getIfNode(time);
+        return projection(time).getIfNode().getBackingNode();
     }
 
     /**
@@ -196,15 +188,7 @@ public DiffNode getIfNode(Time time) {
      * @return the number of annotations above this node at the time {@code time}
      */
     public int getAnnotationDepth(Time time) {
-        if (isRoot()) {
-            return 0;
-        }
-
-        if (isIf()) {
-            return getParent(time).getAnnotationDepth(time) + 1;
-        }
-
-        return getParent(time).getAnnotationDepth(time);
+        return projection(time).getAnnotationDepth();
     }
 
     /**
@@ -212,11 +196,7 @@ public int getAnnotationDepth(Time time) {
      * @return the depth of the this node in the diff tree at the time {@code time}
      */
     public int getDepth(Time time) {
-        if (isRoot()) {
-            return 0;
-        }
-
-        return getParent(time).getDepth(time) + 1;
+        return projection(time).getDepth();
     }
 
     /**
@@ -277,7 +257,7 @@ private void setParent(final DiffNode newParent, Time time) {
     }
 
     /**
-     * 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.
@@ -519,89 +499,9 @@ public List<DiffNode> getAllChildren() {
      * 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.
-     *         The returned list represents a conjunction (i.e., all clauses should be combined with boolean AND).
-     */
-    private List<Node> getFeatureMappingClauses(Time time) {
-        final DiffNode parent = getParent(time);
-
-        if (isElse() || isElif()) {
-            List<Node> 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 = ancestor.getParent(time);
-            }
-            and.add(negate(ancestor.getDirectFeatureMapping()));
-
-            return and;
-        } else if (isArtifact()) {
-            return parent.getFeatureMappingClauses(time);
-        }
-
-        return List.of(getDirectFeatureMapping());
-    }
-
-    /**
-     * Depending on the given time, returns either the feature mapping before or after the edit.
      */
     public Node getFeatureMapping(Time time) {
-        final List<Node> fmClauses = getFeatureMappingClauses(time);
-        if (fmClauses.size() == 1) {
-            return fmClauses.get(0);
-        }
-        return new And(fmClauses);
-    }
-
-    /**
-     * 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 time Whether to return the presence condition clauses 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<Node> getPresenceConditionClauses(Time time) {
-        final DiffNode parent = getParent(time);
-
-        if (isElse() || isElif()) {
-            final List<Node> clauses = new ArrayList<>(getFeatureMappingClauses(time));
-
-            // Find corresponding if
-            DiffNode correspondingIf = parent;
-            while (!correspondingIf.isIf()) {
-                correspondingIf = correspondingIf.getParent(time);
-            }
-
-            // If this elif-else-chain was again nested in another annotation, add its pc.
-            final DiffNode outerNesting = correspondingIf.getParent(time);
-            if (outerNesting != null) {
-                clauses.addAll(outerNesting.getPresenceConditionClauses(time));
-            }
-
-            return clauses;
-        } else if (isArtifact()) {
-            return parent.getPresenceConditionClauses(time);
-        }
-
-        // this is mapping or root
-        final List<Node> clauses;
-        if (parent == null) {
-            clauses = new ArrayList<>(1);
-        } else {
-            clauses = parent.getPresenceConditionClauses(time);
-        }
-        clauses.add(featureMapping);
-        return clauses;
+        return projection(time).getFeatureMapping();
     }
 
     /**
@@ -611,11 +511,7 @@ private List<Node> getPresenceConditionClauses(Time time) {
      * @return The presence condition of this node for the given parent edges.
      */
     public Node getPresenceCondition(Time time) {
-        if (diffType.existsAtTime(time)) {
-            return new And(getPresenceConditionClauses(time));
-        } else {
-            throw new WrongTimeException("Cannot determine after PC of removed node " + this);
-        }
+        return projection(time).getPresenceCondition();
     }
 
     /**

From 25a042fbd73c0c2b13360c06840b3ce96de9fc40 Mon Sep 17 00:00:00 2001
From: Benjamin Moosherr <benjamin.moosherr@uni-ulm.de>
Date: Tue, 8 Nov 2022 15:04:28 +0100
Subject: [PATCH 07/11] Make ID_OFFSET dependent on `DiffType` and `NodeType`

This also avoids duplicating `ID_OFFSET` in both `DiffNode` and
`VariationTreeNode`.
---
 .../diffdetective/diff/difftree/DiffNode.java | 32 +++++++++++--------
 .../diffdetective/diff/difftree/DiffType.java |  7 ++++
 .../diffdetective/diff/difftree/NodeType.java |  7 ++++
 .../diffdetective/util/Assert.java            |  4 +++
 .../variationtree/VariationTreeNode.java      | 32 +++++++++++--------
 src/test/java/StaticAsserts.java              | 23 +++++++++++++
 6 files changed, 79 insertions(+), 26 deletions(-)
 create mode 100644 src/test/java/StaticAsserts.java

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 01add235c..ae2b64cf1 100644
--- a/src/main/java/org/variantsync/diffdetective/diff/difftree/DiffNode.java
+++ b/src/main/java/org/variantsync/diffdetective/diff/difftree/DiffNode.java
@@ -25,8 +25,6 @@
  * @author Paul Bittner, Sören Viegener, Benjamin Moosherr
  */
 public class DiffNode implements HasNodeType {
-    private static final short ID_OFFSET = 3;
-
     /**
      * The diff type of this node, which determines if this node represents
      * an inserted, removed, or unchanged element in a diff.
@@ -589,15 +587,19 @@ public boolean isRoot() {
      */
     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;
     }
 
@@ -609,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 diffTypeBitmask = (1 << DiffType.getRequiredBitCount()) - 1;
+        final int diffTypeOrdinal = id & diffTypeBitmask;
+        id >>= DiffType.getRequiredBitCount();
 
-        final int nodeTypeOrdinal = id & lowestBitsMask;
-        final int diffTypeOrdinal = (id >> ID_OFFSET) & lowestBitsMask;
-        final int fromInDiff      = (id >> (2*ID_OFFSET)) - 1;
+        final int fromInDiff = id - 1;
 
         var nodeType = NodeType.values()[nodeTypeOrdinal];
         return new DiffNode(
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/util/Assert.java b/src/main/java/org/variantsync/diffdetective/util/Assert.java
index 833fe2f8b..fdc16f338 100644
--- a/src/main/java/org/variantsync/diffdetective/util/Assert.java
+++ b/src/main/java/org/variantsync/diffdetective/util/Assert.java
@@ -75,4 +75,8 @@ public static void assertNotNull(Object o) {
             fail("Given object is null but assumed to be not null.");
         }
     }
+
+    public static <T> void assertEquals(T expected, T actual) {
+        assertTrue(expected.equals(actual));
+    }
 }
diff --git a/src/main/java/org/variantsync/diffdetective/variationtree/VariationTreeNode.java b/src/main/java/org/variantsync/diffdetective/variationtree/VariationTreeNode.java
index a2f170df2..1c951dbd7 100644
--- a/src/main/java/org/variantsync/diffdetective/variationtree/VariationTreeNode.java
+++ b/src/main/java/org/variantsync/diffdetective/variationtree/VariationTreeNode.java
@@ -47,8 +47,6 @@
  * @author Benjamin Moosherr
  */
 public class VariationTreeNode extends VariationNode<VariationTreeNode> {
-    private static final short ID_OFFSET = 3;
-
     /**
      * The node type of this node, which determines the type of the represented
      * element in the diff (e.g., mapping or artifact).
@@ -284,16 +282,20 @@ public Node getDirectFeatureMapping() {
     @Override
     public int getID() {
         // Add one to ensure invalid (negative) line numbers don't cause issues.
-        int lineNumber = 1 + from;
-        Assert.assertTrue((lineNumber << 2*ID_OFFSET) >> 2*ID_OFFSET == lineNumber);
+        final int lineNumber = 1 + from;
+
+        final int usedBitCount = DiffType.getRequiredBitCount() + NodeType.getRequiredBitCount();
+        Assert.assertTrue((lineNumber << usedBitCount) >> usedBitCount == lineNumber);
 
         int id;
         id = lineNumber;
-        id <<= ID_OFFSET;
+
         // This makes `VariationTreeNode.toID` compatible with `DiffNode.toID`
-        id += DiffType.NON.ordinal();
-        id <<= ID_OFFSET;
-        id += nodeType.ordinal();
+        id <<= DiffType.getRequiredBitCount();
+        id |= DiffType.NON.ordinal();
+
+        id <<= NodeType.getRequiredBitCount();
+        id |= nodeType.ordinal();
         return id;
     }
 
@@ -305,12 +307,16 @@ public int getID() {
      * @param label the label the node should have
      * @return the reconstructed node
      */
-    public static VariationTreeNode fromID(final int id, List<String> label) {
-        final int lowestBitsMask = (1 << ID_OFFSET) - 1;
+    public static VariationTreeNode fromID(int id, List<String> 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 nodeTypeOrdinal = id & lowestBitsMask;
-        Assert.assertTrue(((id >> ID_OFFSET) & lowestBitsMask) == DiffType.NON.ordinal());
-        final int from = (id >> (2*ID_OFFSET)) - 1;
+        final int from = id - 1;
 
         var nodeType = NodeType.values()[nodeTypeOrdinal];
         return new VariationTreeNode(
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`"
+        );
+    }
+}

From 6262bac0a949bf22c53bb1287b907069e55d0181 Mon Sep 17 00:00:00 2001
From: Benjamin Moosherr <benjamin.moosherr@uni-ulm.de>
Date: Tue, 8 Nov 2022 17:02:28 +0100
Subject: [PATCH 08/11] Remove the unused function `getAnnotationDepth`

It's last usage was removed in 4ac9cdaf3f89675ebd35a1918f52b10fb030a93c.
---
 .../diffdetective/diff/difftree/DiffNode.java     |  9 ---------
 .../variationtree/VariationNode.java              | 15 ---------------
 2 files changed, 24 deletions(-)

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 ae2b64cf1..02957b867 100644
--- a/src/main/java/org/variantsync/diffdetective/diff/difftree/DiffNode.java
+++ b/src/main/java/org/variantsync/diffdetective/diff/difftree/DiffNode.java
@@ -180,15 +180,6 @@ public DiffNode getIfNode(Time time) {
         return projection(time).getIfNode().getBackingNode();
     }
 
-    /**
-     * Gets the number of annotations in the path from the root to this node at the time
-     * {@code time}.
-     * @return the number of annotations above this node at the time {@code time}
-     */
-    public int getAnnotationDepth(Time time) {
-        return projection(time).getAnnotationDepth();
-    }
-
     /**
      * 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}
diff --git a/src/main/java/org/variantsync/diffdetective/variationtree/VariationNode.java b/src/main/java/org/variantsync/diffdetective/variationtree/VariationNode.java
index 344751a24..3bc2588d0 100644
--- a/src/main/java/org/variantsync/diffdetective/variationtree/VariationNode.java
+++ b/src/main/java/org/variantsync/diffdetective/variationtree/VariationNode.java
@@ -176,21 +176,6 @@ public int getDepth() {
         return getParent().getDepth() + 1;
     }
 
-    /**
-     * Computes the number of if-elif-else chains of the path from the root node to this node.
-     */
-    public int getAnnotationDepth() {
-        if (isRoot()) {
-            return 0;
-        }
-
-        if (isIf()) {
-            return getParent().getAnnotationDepth() + 1;
-        }
-
-        return getParent().getAnnotationDepth();
-    }
-
     /**
      * Returns the first {@code if} node in the path from this node upwards to the root.
      */

From 40cf62e53795e370a3cbe4bb7a5146e4ed59fdc6 Mon Sep 17 00:00:00 2001
From: Benjamin Moosherr <benjamin.moosherr@uni-ulm.de>
Date: Tue, 8 Nov 2022 17:07:20 +0100
Subject: [PATCH 09/11] Remove redundant check in `VariationNode.getIfNode`

This check will only be `true` if the root is not an `IF` node, but this
is an invariant so it should never occur. It's better to get the
resulting `NullPointerException` than a `null` return value.
---
 .../variantsync/diffdetective/variationtree/VariationNode.java | 3 ---
 1 file changed, 3 deletions(-)

diff --git a/src/main/java/org/variantsync/diffdetective/variationtree/VariationNode.java b/src/main/java/org/variantsync/diffdetective/variationtree/VariationNode.java
index 3bc2588d0..37d898aa4 100644
--- a/src/main/java/org/variantsync/diffdetective/variationtree/VariationNode.java
+++ b/src/main/java/org/variantsync/diffdetective/variationtree/VariationNode.java
@@ -183,9 +183,6 @@ public VariationNode<T> getIfNode() {
         if (isIf()) {
             return this;
         }
-        if (isRoot()) {
-            return null;
-        }
         return getParent().getIfNode();
     }
 

From 4f09113052d788afd329f79cd91f16e294d9cb80 Mon Sep 17 00:00:00 2001
From: Benjamin Moosherr <benjamin.moosherr@uni-ulm.de>
Date: Tue, 8 Nov 2022 18:47:29 +0100
Subject: [PATCH 10/11] Use the CRTP pattern for `VariationNode`

See https://en.wikipedia.org/wiki/Curiously_recurring_template_pattern
for an explanation of the CRTP pattern.
---
 .../diffdetective/diff/difftree/DiffNode.java |  2 +-
 .../diff/difftree/Projection.java             | 21 +++--
 .../variationtree/VariationNode.java          | 83 ++++++++++---------
 .../variationtree/VariationTreeNode.java      | 28 +++----
 4 files changed, 72 insertions(+), 62 deletions(-)

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 02957b867..f7fa6bd14 100644
--- a/src/main/java/org/variantsync/diffdetective/diff/difftree/DiffNode.java
+++ b/src/main/java/org/variantsync/diffdetective/diff/difftree/DiffNode.java
@@ -743,7 +743,7 @@ public Projection projection(Time time) {
      * 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 <T> DiffNode unchanged(VariationNode<T> variationNode) {
+    public static <T extends VariationNode<T>> DiffNode unchanged(VariationNode<T> variationNode) {
         int from = variationNode.getFromLine();
         int to = variationNode.getToLine();
 
diff --git a/src/main/java/org/variantsync/diffdetective/diff/difftree/Projection.java b/src/main/java/org/variantsync/diffdetective/diff/difftree/Projection.java
index 07202d88f..c6ee20e71 100644
--- a/src/main/java/org/variantsync/diffdetective/diff/difftree/Projection.java
+++ b/src/main/java/org/variantsync/diffdetective/diff/difftree/Projection.java
@@ -28,7 +28,7 @@
  *
  * @see DiffNode#projection
  */
-public class Projection extends VariationNode<DiffNode> {
+public class Projection extends VariationNode<Projection> {
     private DiffNode backingNode;
     private Time time;
 
@@ -50,11 +50,16 @@ public Time getTime() {
         return time;
     }
 
-    @Override
     public DiffNode getBackingNode() {
         return backingNode;
     }
 
+    @Override
+    public Projection upCast() {
+        return this;
+    }
+
+
     @Override
     public NodeType getNodeType() {
         return getBackingNode().nodeType;
@@ -98,7 +103,7 @@ public void setToLine(int to) {
     }
 
     @Override
-    public VariationNode<DiffNode> getParent() {
+    public Projection getParent() {
         var parent = getBackingNode().getParent(time);
 
         if (parent == null) {
@@ -110,7 +115,7 @@ public VariationNode<DiffNode> getParent() {
 
 
     @Override
-    public List<VariationNode<DiffNode>> getChildren() {
+    public List<Projection> getChildren() {
         return FilteredMappedListView.filterMap(
             getBackingNode().getChildOrder(),
             (child) -> {
@@ -124,12 +129,12 @@ public List<VariationNode<DiffNode>> getChildren() {
     }
 
     @Override
-    public void addChild(final VariationNode<DiffNode> child) {
+    public void addChild(final Projection child) {
         getBackingNode().addChild(child.getBackingNode(), time);
     }
 
     @Override
-    public void insertChild(final VariationNode<DiffNode> child, int index) {
+    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`.
@@ -149,7 +154,7 @@ public void insertChild(final VariationNode<DiffNode> child, int index) {
     }
 
     @Override
-    public boolean removeChild(final VariationNode<DiffNode> child) {
+    public boolean removeChild(final Projection child) {
         return getBackingNode().removeChild(child.getBackingNode(), time);
     }
 
@@ -169,7 +174,7 @@ public int getID() {
     }
 
     @Override
-    public boolean isSameAs(VariationNode<DiffNode> other) {
+    public boolean isSameAs(Projection other) {
         if (other != null && getClass() == other.getClass()) {
             Projection otherProjection = (Projection) other;
             return time.equals(otherProjection.time) && getBackingNode() == otherProjection.getBackingNode();
diff --git a/src/main/java/org/variantsync/diffdetective/variationtree/VariationNode.java b/src/main/java/org/variantsync/diffdetective/variationtree/VariationNode.java
index 37d898aa4..f13b5b25c 100644
--- a/src/main/java/org/variantsync/diffdetective/variationtree/VariationNode.java
+++ b/src/main/java/org/variantsync/diffdetective/variationtree/VariationNode.java
@@ -37,20 +37,30 @@
  * {@code getParent() != getParent()}). Instead the method {@link isSameAs} should be used to test
  * for identity.
  *
- * @param <T> the backing type storing the actual information presented by this class. This is
- * currently either a {@link VariationTreeNode concrete variation tree} (in this case
- * {@link getBackingNode} acts as identity) or a {@link Projection} of a {@link DiffNode}.
+ * @param <T> the derived type (the type extending this class)
  *
  * @see assertConsistency
  * @author Benjamin Moosherr
  */
-public abstract class VariationNode<T> implements HasNodeType {
+public abstract class VariationNode<T extends VariationNode<T>> implements HasNodeType {
     /**
-     * Returns the class instance storing the actual attributes of this node. This currently is
-     * either a {@link VariationTreeNode concrete variation tree} or the projection of a
-     * {@link DiffNode} at a specific time.
+     * 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 getBackingNode();
+    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<T> downCast() {
+        return this;
+    }
 
     /**
      * Returns the node type of this node which determines the type of the represented element in
@@ -118,7 +128,7 @@ public Lines getLineRange() {
      * @see drop
      * @see addBelow
      */
-    public abstract VariationNode<T> getParent();
+    public abstract T getParent();
 
     /**
      * Returns an unmodifiable list representing the children of this node.
@@ -134,7 +144,7 @@ public Lines getLineRange() {
      * @see addChild
      * @see removeChild
      */
-    public abstract List<VariationNode<T>> getChildren();
+    public abstract List<T> getChildren();
 
     /**
      * Returns {@code true} iff this node has no parent.
@@ -179,9 +189,9 @@ public int getDepth() {
     /**
      * Returns the first {@code if} node in the path from this node upwards to the root.
      */
-    public VariationNode<T> getIfNode() {
+    public T getIfNode() {
         if (isIf()) {
-            return this;
+            return this.upCast();
         }
         return getParent().getIfNode();
     }
@@ -191,8 +201,8 @@ public VariationNode<T> getIfNode() {
      *
      * @see getChildren
      */
-    public boolean isChild(VariationNode<T> child) {
-        return child.getParent().isSameAs(this);
+    public boolean isChild(T child) {
+        return child.getParent().isSameAs(this.upCast());
     }
 
     /**
@@ -204,7 +214,7 @@ public boolean isChild(VariationNode<T> child) {
      *
      * @see getChildren
      */
-    public int indexOfChild(final VariationNode<T> child) {
+    public int indexOfChild(final T child) {
         return getChildren().indexOf(child);
     }
 
@@ -215,7 +225,7 @@ public int indexOfChild(final VariationNode<T> child) {
      * @throws IllegalArgumentException if {@code child} already has a parent
      * @see getChildren
      */
-    public abstract void addChild(final VariationNode<T> child);
+    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
@@ -227,7 +237,7 @@ public int indexOfChild(final VariationNode<T> child) {
      * @throws IllegalArgumentException if {@code child} already has a parent
      * @see addChildren
      */
-    public abstract void insertChild(final VariationNode<T> child, int index);
+    public abstract void insertChild(final T child, int index);
 
     /**
      * Adds the given nodes in traversal order as children using
@@ -235,7 +245,7 @@ public int indexOfChild(final VariationNode<T> child) {
      * *
      * @throws IllegalArgumentException if any child of {@code children} already has a parent
      */
-    public void addChildren(final Collection<VariationNode<T>> children) {
+    public void addChildren(final Collection<T> children) {
         for (final var child : children) {
             addChild(child);
         }
@@ -251,7 +261,7 @@ public void addChildren(final Collection<VariationNode<T>> children) {
      * @see removeChildren
      * @see getChildren
      */
-    public abstract boolean removeChild(final VariationNode<T> child);
+    public abstract boolean removeChild(final T child);
 
     /**
      * Removes the given nodes from the children list using {@link removeChild}.
@@ -260,7 +270,7 @@ public void addChildren(final Collection<VariationNode<T>> children) {
      * this node
      * @see removeAllChildren
      */
-    public void removeChildren(final Collection<VariationNode<T>> childrenToRemove) {
+    public void removeChildren(final Collection<T> childrenToRemove) {
         for (final var childToRemove : childrenToRemove) {
             removeChild(childToRemove);
         }
@@ -279,9 +289,9 @@ public void removeChildren(final Collection<VariationNode<T>> childrenToRemove)
      * @see addChild
      * @throws IllegalArgumentException if this node already has a parent
      */
-    public void addBelow(final VariationNode<T> parent) {
+    public void addBelow(final T parent) {
         if (parent != null) {
-            parent.addChild(this);
+            parent.addChild(this.upCast());
         }
     }
 
@@ -294,7 +304,7 @@ public void addBelow(final VariationNode<T> parent) {
      */
     public void drop() {
         if (getParent() != null) {
-            getParent().removeChild(this);
+            getParent().removeChild(this.upCast());
         }
     }
 
@@ -306,7 +316,7 @@ public void drop() {
      * @see addChildren
      * @see removeAllChildren
      */
-    public void stealChildrenOf(final VariationNode<T> other) {
+    public void stealChildrenOf(final T other) {
         addChildren(other.getChildren());
         other.removeAllChildren();
     }
@@ -351,7 +361,7 @@ private List<Node> getFeatureMappingClauses() {
 
             return and;
         } else if (isArtifact()) {
-            return parent.getFeatureMappingClauses();
+            return parent.downCast().getFeatureMappingClauses();
         }
 
         return List.of(getDirectFeatureMapping());
@@ -400,12 +410,12 @@ private List<Node> getPresenceConditionClauses() {
             // 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.getPresenceConditionClauses());
+                clauses.addAll(outerNesting.downCast().getPresenceConditionClauses());
             }
 
             return clauses;
         } else if (isArtifact()) {
-            return parent.getPresenceConditionClauses();
+            return parent.downCast().getPresenceConditionClauses();
         }
 
         // this is mapping or root
@@ -413,7 +423,7 @@ private List<Node> getPresenceConditionClauses() {
         if (parent == null) {
             clauses = new ArrayList<>(1);
         } else {
-            clauses = parent.getPresenceConditionClauses();
+            clauses = parent.downCast().getPresenceConditionClauses();
         }
         clauses.add(getDirectFeatureMapping());
         return clauses;
@@ -436,8 +446,8 @@ public Node getPresenceCondition() {
     /**
      * Traverses all nodes in this subtree in preorder.
      */
-    public void forAllPreorder(Consumer<VariationNode<T>> action) {
-        action.accept(this);
+    public void forAllPreorder(Consumer<T> action) {
+        action.accept(this.upCast());
 
         for (var child : getChildren()) {
             child.forAllPreorder(action);
@@ -520,7 +530,7 @@ public void assertConsistency() {
         // check consistency of children lists and edges
         for (var child : getChildren()) {
             Assert.assertTrue(
-                child.getParent().isSameAs(this), () ->
+                child.getParent().isSameAs(this.upCast()), () ->
                 "The parent (" + this + ") of " + child + " is not set correctly");
         }
     }
@@ -541,19 +551,14 @@ public void assertConsistency() {
      * Checks if {@code other} represents the same node as this node.
      *
      * <p>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
-     * {@link getBackingNode backing nodes} and {@link equals} does not.
+     * between {@code ==} and {@link equals}: {@code isSameAs} respects the identity of the backing
+     * node and {@link equals} does not.
      *
      * <p>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(VariationNode<T> other);
-
-    @Override
-    public String toString() {
-        return getBackingNode().toString();
-    }
+    public abstract boolean isSameAs(T other);
 
     /**
      * Unparses the labels of this subtree into {@code output}.
diff --git a/src/main/java/org/variantsync/diffdetective/variationtree/VariationTreeNode.java b/src/main/java/org/variantsync/diffdetective/variationtree/VariationTreeNode.java
index 1c951dbd7..3bebf2ec0 100644
--- a/src/main/java/org/variantsync/diffdetective/variationtree/VariationTreeNode.java
+++ b/src/main/java/org/variantsync/diffdetective/variationtree/VariationTreeNode.java
@@ -158,7 +158,7 @@ public static VariationTreeNode createArtifact(int fromLine, int toLine, List<St
     }
 
     @Override
-    public VariationTreeNode getBackingNode() {
+    public VariationTreeNode upCast() {
         return this;
     }
 
@@ -215,7 +215,7 @@ public void setToLine(int to) {
     }
 
     @Override
-    public VariationNode<VariationTreeNode> getParent() {
+    public VariationTreeNode getParent() {
         return parent;
     }
 
@@ -230,27 +230,27 @@ private void setParent(final VariationTreeNode newParent) {
     }
 
     @Override
-    public List<VariationNode<VariationTreeNode>> getChildren() {
+    public List<VariationTreeNode> getChildren() {
         return Collections.unmodifiableList(childOrder);
     }
 
     @Override
-    public void addChild(final VariationNode<VariationTreeNode> child) {
-        child.getBackingNode().setParent(this);
-        childOrder.add(child.getBackingNode());
+    public void addChild(final VariationTreeNode child) {
+        child.setParent(this);
+        childOrder.add(child);
     }
 
     @Override
-    public void insertChild(final VariationNode<VariationTreeNode> child, int index) {
-        child.getBackingNode().setParent(this);
-        childOrder.add(index, child.getBackingNode());
+    public void insertChild(final VariationTreeNode child, int index) {
+        child.setParent(this);
+        childOrder.add(index, child);
     }
 
     @Override
-    public boolean removeChild(final VariationNode<VariationTreeNode> child) {
+    public boolean removeChild(final VariationTreeNode child) {
         if (isChild(child)) {
-            child.getBackingNode().parent = null;
-            childOrder.remove(child.getBackingNode());
+            child.parent = null;
+            childOrder.remove(child);
             return true;
         }
         return false;
@@ -259,7 +259,7 @@ public boolean removeChild(final VariationNode<VariationTreeNode> child) {
     @Override
     public void removeAllChildren() {
         for (var child : childOrder) {
-            child.getBackingNode().parent = null;
+            child.parent = null;
         }
 
         childOrder.clear();
@@ -329,7 +329,7 @@ public static VariationTreeNode fromID(int id, List<String> label) {
     }
 
     @Override
-    public boolean isSameAs(VariationNode<VariationTreeNode> other) {
+    public boolean isSameAs(VariationTreeNode other) {
         return this == other;
     }
 

From a3a2a513e84907e6c5ce0833692c6a7b552fb011 Mon Sep 17 00:00:00 2001
From: Benjamin Moosherr <benjamin.moosherr@uni-ulm.de>
Date: Tue, 8 Nov 2022 19:19:21 +0100
Subject: [PATCH 11/11] Use `Lines` instead a `from` and `to` line in
 `VariationNode`

---
 .../diffdetective/diff/DiffLineNumber.java    | 16 +++++
 .../diffdetective/diff/difftree/DiffNode.java | 13 +++-
 .../diff/difftree/Projection.java             | 35 ++--------
 .../variationtree/VariationNode.java          | 48 +++-----------
 .../variationtree/VariationTreeNode.java      | 66 +++++++------------
 5 files changed, 65 insertions(+), 113 deletions(-)

diff --git a/src/main/java/org/variantsync/diffdetective/diff/DiffLineNumber.java b/src/main/java/org/variantsync/diffdetective/diff/DiffLineNumber.java
index 048a845cf..fbb48caae 100644
--- a/src/main/java/org/variantsync/diffdetective/diff/DiffLineNumber.java
+++ b/src/main/java/org/variantsync/diffdetective/diff/DiffLineNumber.java
@@ -40,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.
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 f7fa6bd14..b7e72e9e6 100644
--- a/src/main/java/org/variantsync/diffdetective/diff/difftree/DiffNode.java
+++ b/src/main/java/org/variantsync/diffdetective/diff/difftree/DiffNode.java
@@ -455,6 +455,15 @@ public Lines getLinesAtTime(Time time) {
         return DiffLineNumber.rangeAtTime(from, to, time);
     }
 
+    /**
+     * Returns the range of line numbers of this node's corresponding source code before or after
+     * the edit.
+     */
+    public void setLinesAtTime(Lines lines, Time time) {
+        from = from.withLineNumberAtTime(lines.getFromInclusive(), time);
+        to = to.withLineNumberAtTime(lines.getToExclusive(), time);
+    }
+
     /**
      * Returns the formula that is stored in this node.
      * The formula is null for artifact nodes (i.e., {@link NodeType#ARTIFACT}).
@@ -744,8 +753,8 @@ public Projection projection(Time time) {
      * (all node had a {@link getDiffType diff type} of {@link DiffType#NON}).
      */
     public static <T extends VariationNode<T>> DiffNode unchanged(VariationNode<T> variationNode) {
-        int from = variationNode.getFromLine();
-        int to = variationNode.getToLine();
+        int from = variationNode.getLineRange().getFromInclusive();
+        int to = variationNode.getLineRange().getToExclusive();
 
         var diffNode = new DiffNode(
             DiffType.NON,
diff --git a/src/main/java/org/variantsync/diffdetective/diff/difftree/Projection.java b/src/main/java/org/variantsync/diffdetective/diff/difftree/Projection.java
index c6ee20e71..e372c973b 100644
--- a/src/main/java/org/variantsync/diffdetective/diff/difftree/Projection.java
+++ b/src/main/java/org/variantsync/diffdetective/diff/difftree/Projection.java
@@ -4,13 +4,10 @@
 import java.util.Optional;
 
 import org.prop4j.Node;
-import org.variantsync.diffdetective.diff.DiffLineNumber;
+import org.variantsync.diffdetective.diff.Lines;
 import org.variantsync.diffdetective.variationtree.VariationNode;
 import org.variantsync.functjonal.list.FilteredMappedListView;
 
-import static org.variantsync.diffdetective.diff.difftree.Time.AFTER;
-import static org.variantsync.diffdetective.diff.difftree.Time.BEFORE;
-
 /**
  * A view of a {@link DiffNode} as variation node at a specific time.
  *
@@ -71,35 +68,13 @@ public List<String> getLabelLines() {
     }
 
     @Override
-    public int getFromLine() {
-        return getBackingNode().getFromLine().atTime(time);
-    }
-
-    @Override
-    public void setFromLine(int from) {
-        var node = getBackingNode();
-        var current = node.getFromLine();
-        node.setFromLine(new DiffLineNumber(
-            current.inDiff(),
-            time == BEFORE ? from : current.beforeEdit(),
-            time == AFTER ? from : current.afterEdit()
-        ));
-    }
-
-    @Override
-    public int getToLine() {
-        return getBackingNode().getToLine().atTime(time);
+    public Lines getLineRange() {
+        return getBackingNode().getLinesAtTime(time);
     }
 
     @Override
-    public void setToLine(int to) {
-        var node = getBackingNode();
-        var current = node.getToLine();
-        node.setToLine(new DiffLineNumber(
-            current.inDiff(),
-            time == BEFORE ? to : current.beforeEdit(),
-            time == AFTER ? to : current.afterEdit()
-        ));
+    public void setLineRange(Lines lineRange) {
+        getBackingNode().setLinesAtTime(lineRange, time);
     }
 
     @Override
diff --git a/src/main/java/org/variantsync/diffdetective/variationtree/VariationNode.java b/src/main/java/org/variantsync/diffdetective/variationtree/VariationNode.java
index f13b5b25c..b36647591 100644
--- a/src/main/java/org/variantsync/diffdetective/variationtree/VariationNode.java
+++ b/src/main/java/org/variantsync/diffdetective/variationtree/VariationNode.java
@@ -81,46 +81,18 @@ public VariationNode<T> downCast() {
     public abstract List<String> getLabelLines();
 
     /**
-     * Returns the start line number of the {@link getLabelLines label} of this node in the
-     * corresponding source code.
-     *
-     * @see setFromLine
-     */
-    public abstract int getFromLine();
-
-    /**
-     * Changes the line number returned by {@link getFromLine}.
-     *
-     * @see getFromLine
-     */
-    public abstract void setFromLine(int from);
-
-    /**
-     * Returns the end line number of the {@link getLabelLines label} of this node in the
-     * corresponding source code.
-     *
-     * <p>The line number is exclusive (i.e., it points 1 behind the last included line).
-     *
-     * @see setToLine
-     */
-    public abstract int getToLine();
-
-    /**
-     * Changes the line number returned by {@link getToLine}.
+     * Returns the range of line numbers of this node's corresponding source code.
      *
-     * @see getToLine
+     * @see setLineRange
      */
-    public abstract void setToLine(int to);
+    public abstract Lines getLineRange();
 
     /**
-     * Returns the range of line numbers of this node's corresponding source code.
+     * Sets the range of line numbers of this node's corresponding source code.
      *
-     * @see getFromLine
-     * @see getToLine
+     * @see getLineRange
      */
-    public Lines getLineRange() {
-        return Lines.FromInclToExcl(getFromLine(), getToLine());
-    }
+    public abstract void setLineRange(Lines lineRange);
 
     /**
      * Returns the parent of this node, or {@code null} if this node doesn't have a parent.
@@ -463,9 +435,8 @@ public VariationTreeNode toVariationTree() {
         // Copy mutable attributes to allow modifications of the new node.
         var newNode = new VariationTreeNode(
             getNodeType(),
-            getFromLine(),
-            getToLine(),
             getDirectFeatureMapping().clone(),
+            getLineRange(),
             new ArrayList<String>(getLabelLines())
         );
 
@@ -542,8 +513,9 @@ public void assertConsistency() {
      *
      * <p>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 getFromLine}. Beware that {@link Projection} returns {@link DiffNode#getID} so this
-     * id is not compatible with {@link VariationTreeNode#getID}.
+     * {@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();
 
diff --git a/src/main/java/org/variantsync/diffdetective/variationtree/VariationTreeNode.java b/src/main/java/org/variantsync/diffdetective/variationtree/VariationTreeNode.java
index 3bebf2ec0..88ed97f6c 100644
--- a/src/main/java/org/variantsync/diffdetective/variationtree/VariationTreeNode.java
+++ b/src/main/java/org/variantsync/diffdetective/variationtree/VariationTreeNode.java
@@ -2,6 +2,7 @@
 
 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;
@@ -51,17 +52,12 @@ public class VariationTreeNode extends VariationNode<VariationTreeNode> {
      * The node type of this node, which determines the type of the represented
      * element in the diff (e.g., mapping or artifact).
      */
-    public final NodeType nodeType;
+    private final NodeType nodeType;
 
     /**
-     * The start line number of the {@link label} of this node in the corresponding source code.
+     * The range of line numbers of this node's corresponding source code.
      */
-    private int from = DiffLineNumber.InvalidLineNumber;
-    /**
-     * The end line number of the {@link label} of this node in the corresponding source code.
-     * The line number is exclusive (i.e., it points 1 behind the last included line).
-     */
-    private int to = DiffLineNumber.InvalidLineNumber;
+    private Lines lineRange;
 
     /**
      * A list of lines representing the label of this node.
@@ -98,26 +94,23 @@ public class VariationTreeNode extends VariationNode<VariationTreeNode> {
      * modifications to this list.
      *
      * @param nodeType the type of this node
-     * @param fromLine the start line number of the code of {@code label}
-     * @param toLine the end line number (exclusive) of the code of {@code label}
      * @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,
-        int fromLine,
-        int toLine,
         Node featureMapping,
+        Lines lineRange,
         List<String> label
     ) {
         super();
 
         this.nodeType = nodeType;
-        this.from = fromLine;
-        this.to = toLine;
+        this.lineRange = lineRange;
         this.label = label;
         this.featureMapping = featureMapping;
 
@@ -138,9 +131,8 @@ public VariationTreeNode(
     public static VariationTreeNode createRoot() {
         return new VariationTreeNode(
                 NodeType.IF,
-                DiffLineNumber.InvalidLineNumber,
-                DiffLineNumber.InvalidLineNumber,
                 FixTrueFalse.True,
+                Lines.Invalid(),
                 new ArrayList<>()
         );
     }
@@ -148,13 +140,12 @@ public static VariationTreeNode createRoot() {
     /**
      * Convenience constructor for creating an artifact node of a {@link VariationTree}.
      *
-     * @param fromLine the start line number of the code of {@code label}
-     * @param toLine the end line number (exclusive) of the code of {@code label}
+     * @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(int fromLine, int toLine, List<String> label) {
-        return new VariationTreeNode(NodeType.ARTIFACT, fromLine, toLine, null, label);
+    public static VariationTreeNode createArtifact(Lines lineRange, List<String> label) {
+        return new VariationTreeNode(NodeType.ARTIFACT, null, lineRange, label);
     }
 
     @Override
@@ -195,23 +186,13 @@ public void addLines(final List<String> lines) {
     }
 
     @Override
-    public int getFromLine() {
-        return from;
-    }
-
-    @Override
-    public void setFromLine(int from) {
-        this.from = from;
-    }
-
-    @Override
-    public int getToLine() {
-        return to;
+    public Lines getLineRange() {
+        return lineRange;
     }
 
     @Override
-    public void setToLine(int to) {
-        this.to = to;
+    public void setLineRange(Lines lineRange) {
+        this.lineRange = lineRange;
     }
 
     @Override
@@ -274,7 +255,7 @@ public Node getDirectFeatureMapping() {
      * Returns an integer that uniquely identifiers this node within its variation tree.
      *
      * <p>From the returned id a new node with all essential attributes ({@link getNodeType node
-     * type} and {@link getFromLine start line number}) can be reconstructed by using
+     * type} and {@link getLineRange start line number}) can be reconstructed by using
      * {@link fromID}.
      *
      * <p>Note that this encoding assumes that line numbers fit into {@code 26} bits.
@@ -282,7 +263,7 @@ public Node getDirectFeatureMapping() {
     @Override
     public int getID() {
         // Add one to ensure invalid (negative) line numbers don't cause issues.
-        final int lineNumber = 1 + from;
+        final int lineNumber = 1 + getLineRange().getFromInclusive();
 
         final int usedBitCount = DiffType.getRequiredBitCount() + NodeType.getRequiredBitCount();
         Assert.assertTrue((lineNumber << usedBitCount) >> usedBitCount == lineNumber);
@@ -321,9 +302,8 @@ public static VariationTreeNode fromID(int id, List<String> label) {
         var nodeType = NodeType.values()[nodeTypeOrdinal];
         return new VariationTreeNode(
                 nodeType,
-                from,
-                DiffLineNumber.InvalidLineNumber,
                 nodeType.isConditionalAnnotation() ? FixTrueFalse.True : null,
+                Lines.SingleLine(from),
                 label
         );
     }
@@ -338,7 +318,7 @@ 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 && from == other.from && to == other.to && Objects.equals(featureMapping, other.featureMapping) && label.equals(other.label);
+        return nodeType == other.nodeType && lineRange.equals(other.lineRange) && Objects.equals(featureMapping, other.featureMapping) && label.equals(other.label);
     }
 
     /**
@@ -351,19 +331,19 @@ public boolean equals(Object o) {
      */
     @Override
     public int hashCode() {
-        return Objects.hash(nodeType, from, to, featureMapping, label);
+        return Objects.hash(nodeType, lineRange, featureMapping, label);
     }
 
     @Override
     public String toString() {
         String s;
         if (isArtifact()) {
-            s = String.format("%s from %d to %d", nodeType, from, to);
+            s = String.format("%s in the lines %d", nodeType, lineRange);
         } else if (isRoot()) {
             s = "ROOT";
         } else {
-            s = String.format("%s from %d to %d with \"%s\"", nodeType,
-                    from, to, featureMapping);
+            s = String.format("%s in the lines %d with \"%s\"", nodeType,
+                    lineRange, featureMapping);
         }
         return s;
     }
