-
Notifications
You must be signed in to change notification settings - Fork 8
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[fact] Refactor ContentMerger to use configurable handlers, fix #142
- Loading branch information
Showing
11 changed files
with
524 additions
and
324 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
40 changes: 40 additions & 0 deletions
40
src/main/java/se/kth/spork/spoon/conflict/CommentContentHandler.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
|
||
} |
48 changes: 48 additions & 0 deletions
48
src/main/java/se/kth/spork/spoon/conflict/ContentConflictHandler.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
161
src/main/java/se/kth/spork/spoon/conflict/ContentMerger.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
38
src/main/java/se/kth/spork/spoon/conflict/IsImplicitHandler.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
60
src/main/java/se/kth/spork/spoon/conflict/IsUpperHandler.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
} | ||
|
||
} |
Oops, something went wrong.