Skip to content

Commit

Permalink
[fact] Refactor ContentMerger to use configurable handlers, fix #142
Browse files Browse the repository at this point in the history
  • Loading branch information
slarse committed May 27, 2020
1 parent acaf26d commit 1f2661b
Show file tree
Hide file tree
Showing 11 changed files with 524 additions and 324 deletions.
14 changes: 12 additions & 2 deletions src/main/java/se/kth/spork/spoon/Spoon3dmMerge.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@
import com.github.gumtreediff.tree.ITree;
import gumtree.spoon.builder.SpoonGumTreeBuilder;
import se.kth.spork.base3dm.*;
import se.kth.spork.spoon.conflict.CommentContentHandler;
import se.kth.spork.spoon.conflict.ContentConflictHandler;
import se.kth.spork.spoon.conflict.IsImplicitHandler;
import se.kth.spork.spoon.conflict.IsUpperHandler;
import se.kth.spork.spoon.conflict.ModifierHandler;
import se.kth.spork.spoon.conflict.OptimisticInsertInsertHandler;
import se.kth.spork.spoon.conflict.StructuralConflict;
import se.kth.spork.spoon.matching.ClassRepresentatives;
Expand All @@ -20,6 +25,7 @@
import se.kth.spork.util.LineBasedMerge;
import se.kth.spork.util.Pair;
import spoon.reflect.declaration.*;
import spoon.reflect.path.CtRole;

import java.nio.file.Path;
import java.util.*;
Expand Down Expand Up @@ -132,8 +138,12 @@ public static <T extends CtElement> Pair<T, Integer> merge(

// INTERPRETER PHASE
LOGGER.info(() -> "Interpreting resolved PCS merge");
List<StructuralConflictHandler> conflictHandlers = Arrays.asList(new MethodOrderingConflictHandler(), new OptimisticInsertInsertHandler());
Pair<CtElement, Integer> merge = PcsInterpreter.fromMergedPcs(delta, baseLeft, baseRight, conflictHandlers);
List<StructuralConflictHandler> structuralConflictHandlers = Arrays.asList(
new MethodOrderingConflictHandler(), new OptimisticInsertInsertHandler());
List<ContentConflictHandler> contentConflictHandlers = Arrays.asList(
new IsImplicitHandler(), new ModifierHandler(), new IsUpperHandler(), new CommentContentHandler());
Pair<CtElement, Integer> merge = PcsInterpreter.fromMergedPcs(
delta, baseLeft, baseRight, structuralConflictHandlers, contentConflictHandlers);
// we can be certain that the merge tree has the same root type as the three constituents, so this cast is safe
@SuppressWarnings("unchecked")
T mergeTree = (T) merge.first;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package se.kth.spork.spoon.conflict;

import se.kth.spork.util.LineBasedMerge;
import se.kth.spork.util.Pair;
import spoon.reflect.declaration.CtElement;
import spoon.reflect.path.CtRole;

import java.util.Optional;

/**
* A conflict handler for comment contents.
*
* @author Simon Larsén
*/
public class CommentContentHandler implements ContentConflictHandler {
@Override
public CtRole getRole() {
return CtRole.COMMENT_CONTENT;
}

@Override
public Pair<Optional<Object>, Boolean> handleConflict(
Optional<Object> baseVal,
Object leftVal,
Object rightVal,
Optional<CtElement> baseElem,
CtElement leftElem,
CtElement rightElem) {
return Pair.of(mergeComments(baseVal.orElse(""), leftVal, rightVal), false);
}

private static Optional<Object> mergeComments(Object base, Object left, Object right) {
Pair<String, Integer> merge = LineBasedMerge.merge(base.toString(), left.toString(), right.toString());
if (merge.second > 0) {
return Optional.empty();
}
return Optional.of(merge.first);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package se.kth.spork.spoon.conflict;

import se.kth.spork.util.Pair;
import spoon.reflect.declaration.CtElement;
import spoon.reflect.path.CtRole;

import java.util.Optional;

/**
* Interface that defines a content conflict handler.
*
* @author Simon Larsén
*/
public interface ContentConflictHandler {

/**
* Handle a content conflict.
*
* The boolean in the return value should be true if the content was partially merged, and the optional value
* must then be non-empty.
*
* If the content was not merged at all, then the boolean should be false, and the value should be an empty optional.
*
* If the content was fully merged, then the boolean should be false and the value should be a non-empty optional.
*
* @param baseVal The value from the base revision. Not always present.
* @param leftVal The value from the left revision.
* @param rightVal The value from the right revision.
* @param baseElem The base element, from which the base value was taken. Not always present.
* @param leftElem The left element, from which the left value was taken.
* @param rightElem The right element, from which the right value was taken.
* @return A pair (mergedContent, isPartiallyMerged).
*/
Pair<Optional<Object>, Boolean>
handleConflict(
Optional<Object> baseVal,
Object leftVal,
Object rightVal,
Optional<CtElement> baseElem,
CtElement leftElem,
CtElement rightElem);

/**
* @return The role that this conflict handler deals with.
*/
CtRole getRole();

}
161 changes: 161 additions & 0 deletions src/main/java/se/kth/spork/spoon/conflict/ContentMerger.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
package se.kth.spork.spoon.conflict;

import se.kth.spork.base3dm.Content;
import se.kth.spork.spoon.wrappers.RoledValue;
import se.kth.spork.spoon.wrappers.RoledValues;
import se.kth.spork.spoon.wrappers.SpoonNode;
import se.kth.spork.util.Pair;
import se.kth.spork.util.Triple;
import spoon.reflect.path.CtRole;

import java.util.*;

/**
* A class for dealing with merging of content.
*
* @author Simon Larsén
*/
public class ContentMerger {
private final Map<CtRole, ContentConflictHandler> conflictHandlers;

/**
* @param conflictHandlers A list of conflict handlers. There may only be one handler per role.
*/
public ContentMerger(List<ContentConflictHandler> conflictHandlers) {
this.conflictHandlers = new HashMap<>();
for (ContentConflictHandler handler : conflictHandlers) {
if (this.conflictHandlers.containsKey(handler.getRole())) {
throw new IllegalArgumentException("duplicate handler for role " + handler.getRole());
}
this.conflictHandlers.put(handler.getRole(), handler);
}
}

/**
* @param nodeContents The contents associated with this node.
* @return A pair of merged contents and a potentially empty collection of unresolved conflicts.
*/
@SuppressWarnings("unchecked")
public Pair<RoledValues, List<ContentConflict>>
mergedContent(Set<Content<SpoonNode, RoledValues>> nodeContents) {
if (nodeContents.size() == 1) {
return Pair.of(nodeContents.iterator().next().getValue(), Collections.emptyList());
}

_ContentTriple revisions = getContentRevisions(nodeContents);
Optional<Content<SpoonNode, RoledValues>> baseOpt = revisions.first;
RoledValues leftRoledValues = revisions.second.getValue();
RoledValues rightRoledValues = revisions.third.getValue();

// NOTE: It is important that the left values are copied,
// by convention the LEFT values should be put into the tree whenever a conflict cannot be resolved
RoledValues mergedRoledValues = new RoledValues(leftRoledValues);

assert leftRoledValues.size() == rightRoledValues.size();

Deque<ContentConflict> unresolvedConflicts = new ArrayDeque<>();

for (int i = 0; i < leftRoledValues.size(); i++) {
int finalI = i;
RoledValue leftRv = leftRoledValues.get(i);
RoledValue rightRv = rightRoledValues.get(i);
assert leftRv.getRole() == rightRv.getRole();

Optional<RoledValue> baseRv = baseOpt.map(Content::getValue).map(rv -> rv.get(finalI));
Optional<Object> baseValOpt = baseRv.map(RoledValue::getValue);

CtRole role = leftRv.getRole();
Object leftVal = leftRv.getValue();
Object rightVal = rightRv.getValue();

if (leftRv.equals(rightRv)) {
// this pair cannot possibly conflict
continue;
}

// left and right pairs differ and are so conflicting
// we add them as a conflict, but will later remove it if the conflict can be resolved
unresolvedConflicts.push(new ContentConflict(
role,
baseOpt.map(Content::getValue).map(rv -> rv.get(finalI)),
leftRv,
rightRv));


Optional<?> merged = Optional.empty();

// sometimes a value can be partially merged (e.g. modifiers), and then we want to be
// able to set the merged value, AND flag a conflict.
boolean conflictPresent = false;

// if either value is equal to base, we keep THE OTHER one
if (baseValOpt.isPresent() && baseValOpt.get().equals(leftVal)) {
merged = Optional.of(rightVal);
} else if (baseValOpt.isPresent() && baseValOpt.get().equals(rightVal)) {
merged = Optional.of(leftVal);
} else {
// non-trivial conflict, check if there is a conflict handler for this role
ContentConflictHandler handler = conflictHandlers.get(role);
if (handler != null) {
Pair<Optional<Object>, Boolean> result = handler.handleConflict(
baseValOpt,
leftVal,
rightVal,
baseOpt.map(c -> c.getValue().getElement()),
leftRoledValues.getElement(),
rightRoledValues.getElement());
merged = result.first;
conflictPresent = result.second;
}
}


if (merged.isPresent()) {
mergedRoledValues.set(i, role, merged.get());

if (!conflictPresent)
unresolvedConflicts.pop();
}
}

return Pair.of(mergedRoledValues, new ArrayList<>(unresolvedConflicts));
}

private static _ContentTriple getContentRevisions(Set<Content<SpoonNode, RoledValues>> contents) {
Content<SpoonNode, RoledValues> base = null;
Content<SpoonNode, RoledValues> left = null;
Content<SpoonNode, RoledValues> right = null;

for (Content<SpoonNode, RoledValues> cnt : contents) {
switch (cnt.getRevision()) {
case BASE:
base = cnt;
break;
case LEFT:
left = cnt;
break;
case RIGHT:
right = cnt;
break;
}
}

if (left == null || right == null)
throw new IllegalArgumentException("Expected at least left and right revisions, got: " + contents);

return new _ContentTriple(Optional.ofNullable(base), left, right);
}

// this is just a type alias to declutter the getContentRevisions method header
private static class _ContentTriple extends Triple<
Optional<Content<SpoonNode, RoledValues>>,
Content<SpoonNode, RoledValues>,
Content<SpoonNode, RoledValues>> {
public _ContentTriple(
Optional<Content<SpoonNode, RoledValues>> first,
Content<SpoonNode, RoledValues> second,
Content<SpoonNode, RoledValues> third) {
super(first, second, third);
}
}
}
38 changes: 38 additions & 0 deletions src/main/java/se/kth/spork/spoon/conflict/IsImplicitHandler.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package se.kth.spork.spoon.conflict;

import se.kth.spork.util.Pair;
import spoon.reflect.declaration.CtElement;
import spoon.reflect.path.CtRole;

import java.util.Optional;

/**
* Conflict handler for the IS_IMPLICIT attribute.
*
* @author Simon Larsén
*/
public class IsImplicitHandler implements ContentConflictHandler {
@Override
public CtRole getRole() {
return CtRole.IS_IMPLICIT;
}

@Override
public Pair<Optional<Object>, Boolean> handleConflict(
Optional<Object> baseVal,
Object leftVal,
Object rightVal,
Optional<CtElement> baseElem,
CtElement leftElem,
CtElement rightElem) {
if (baseVal.isPresent()) {
// as there are only two possible values for a boolean, left and right disagreeing must mean that the base
// value has been changed
Boolean change = !(Boolean) baseVal.get();
return Pair.of(Optional.of(change), false);
} else {
// left and right disagree and base is unavailable; discarding implicitness most often works
return Pair.of(Optional.of(false), false);
}
}
}
60 changes: 60 additions & 0 deletions src/main/java/se/kth/spork/spoon/conflict/IsUpperHandler.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package se.kth.spork.spoon.conflict;

import se.kth.spork.util.Pair;
import spoon.reflect.declaration.CtElement;
import spoon.reflect.path.CtRole;
import spoon.reflect.reference.CtWildcardReference;

import java.util.Optional;

/**
* A conflict handler for the IS_UPPER attribute. This appears on wildcards to specify if a type bound is an upper
* or a lower type bound. For example <code><? extends String></code> means that IS_UPPER is true,
* and <code><? super String></code> means that IS_UPPER is false.
*
* @author Simon Larsén
*/
public class IsUpperHandler implements ContentConflictHandler {

@Override
public CtRole getRole() {
return CtRole.IS_UPPER;
}

@Override
public Pair<Optional<Object>, Boolean> handleConflict(
Optional<Object> baseVal,
Object leftVal,
Object rightVal,
Optional<CtElement> baseElem,
CtElement leftElem,
CtElement rightElem) {
return Pair.of(mergeIsUpper(baseElem, leftElem, rightElem), false);
}

private static Optional<Object> mergeIsUpper(Optional<CtElement> baseElem, CtElement leftElem, CtElement rightElem) {
CtWildcardReference left = (CtWildcardReference) leftElem;
CtWildcardReference right = (CtWildcardReference) rightElem;

boolean leftBoundIsImplicit = left.getBoundingType().isImplicit();
boolean rightBoundIsImplicit = right.getBoundingType().isImplicit();

if (baseElem.isPresent()) {
CtWildcardReference base = (CtWildcardReference) baseElem.get();
boolean baseBoundIsImplicit = base.getBoundingType().isImplicit();

if (leftBoundIsImplicit != rightBoundIsImplicit) {
// one bound was removed, so we go with whatever is on the bound that is not equal to base
return Optional.of(baseBoundIsImplicit == leftBoundIsImplicit ? left.isUpper() : right.isUpper());
}
} else {
if (leftBoundIsImplicit != rightBoundIsImplicit) {
// only one bound implicit, pick isUpper of the explicit one
return Optional.of(leftBoundIsImplicit ? left.isUpper() : right.isUpper());
}
}

return Optional.empty();
}

}
Loading

0 comments on commit 1f2661b

Please sign in to comment.