From 1f2661be35b668ae1f2307e5b4b128f855eb1f94 Mon Sep 17 00:00:00 2001 From: slarse Date: Wed, 27 May 2020 21:39:01 +0200 Subject: [PATCH] [fact] Refactor ContentMerger to use configurable handlers, fix #142 --- .../se/kth/spork/spoon/Spoon3dmMerge.java | 14 +- .../spoon/conflict/CommentContentHandler.java | 40 +++ .../conflict/ContentConflictHandler.java | 48 +++ .../spork/spoon/conflict/ContentMerger.java | 161 +++++++++ .../spoon/conflict/IsImplicitHandler.java | 38 +++ .../spork/spoon/conflict/IsUpperHandler.java | 60 ++++ .../spork/spoon/conflict/ModifierHandler.java | 145 ++++++++ .../spoon/pcsinterpreter/ContentMerger.java | 311 ------------------ .../spoon/pcsinterpreter/PcsInterpreter.java | 7 +- .../pcsinterpreter/SpoonTreeBuilder.java | 18 +- .../spoon/printer/PrinterPreprocessor.java | 6 +- 11 files changed, 524 insertions(+), 324 deletions(-) create mode 100644 src/main/java/se/kth/spork/spoon/conflict/CommentContentHandler.java create mode 100644 src/main/java/se/kth/spork/spoon/conflict/ContentConflictHandler.java create mode 100644 src/main/java/se/kth/spork/spoon/conflict/ContentMerger.java create mode 100644 src/main/java/se/kth/spork/spoon/conflict/IsImplicitHandler.java create mode 100644 src/main/java/se/kth/spork/spoon/conflict/IsUpperHandler.java create mode 100644 src/main/java/se/kth/spork/spoon/conflict/ModifierHandler.java delete mode 100644 src/main/java/se/kth/spork/spoon/pcsinterpreter/ContentMerger.java diff --git a/src/main/java/se/kth/spork/spoon/Spoon3dmMerge.java b/src/main/java/se/kth/spork/spoon/Spoon3dmMerge.java index b9b992ac..9f109aa2 100644 --- a/src/main/java/se/kth/spork/spoon/Spoon3dmMerge.java +++ b/src/main/java/se/kth/spork/spoon/Spoon3dmMerge.java @@ -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; @@ -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.*; @@ -132,8 +138,12 @@ public static Pair merge( // INTERPRETER PHASE LOGGER.info(() -> "Interpreting resolved PCS merge"); - List conflictHandlers = Arrays.asList(new MethodOrderingConflictHandler(), new OptimisticInsertInsertHandler()); - Pair merge = PcsInterpreter.fromMergedPcs(delta, baseLeft, baseRight, conflictHandlers); + List structuralConflictHandlers = Arrays.asList( + new MethodOrderingConflictHandler(), new OptimisticInsertInsertHandler()); + List contentConflictHandlers = Arrays.asList( + new IsImplicitHandler(), new ModifierHandler(), new IsUpperHandler(), new CommentContentHandler()); + Pair 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; diff --git a/src/main/java/se/kth/spork/spoon/conflict/CommentContentHandler.java b/src/main/java/se/kth/spork/spoon/conflict/CommentContentHandler.java new file mode 100644 index 00000000..aaee37f3 --- /dev/null +++ b/src/main/java/se/kth/spork/spoon/conflict/CommentContentHandler.java @@ -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, Boolean> handleConflict( + Optional baseVal, + Object leftVal, + Object rightVal, + Optional baseElem, + CtElement leftElem, + CtElement rightElem) { + return Pair.of(mergeComments(baseVal.orElse(""), leftVal, rightVal), false); + } + + private static Optional mergeComments(Object base, Object left, Object right) { + Pair merge = LineBasedMerge.merge(base.toString(), left.toString(), right.toString()); + if (merge.second > 0) { + return Optional.empty(); + } + return Optional.of(merge.first); + } + +} diff --git a/src/main/java/se/kth/spork/spoon/conflict/ContentConflictHandler.java b/src/main/java/se/kth/spork/spoon/conflict/ContentConflictHandler.java new file mode 100644 index 00000000..ecc76116 --- /dev/null +++ b/src/main/java/se/kth/spork/spoon/conflict/ContentConflictHandler.java @@ -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, Boolean> + handleConflict( + Optional baseVal, + Object leftVal, + Object rightVal, + Optional baseElem, + CtElement leftElem, + CtElement rightElem); + + /** + * @return The role that this conflict handler deals with. + */ + CtRole getRole(); + +} diff --git a/src/main/java/se/kth/spork/spoon/conflict/ContentMerger.java b/src/main/java/se/kth/spork/spoon/conflict/ContentMerger.java new file mode 100644 index 00000000..dd053be5 --- /dev/null +++ b/src/main/java/se/kth/spork/spoon/conflict/ContentMerger.java @@ -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 conflictHandlers; + + /** + * @param conflictHandlers A list of conflict handlers. There may only be one handler per role. + */ + public ContentMerger(List 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> + mergedContent(Set> nodeContents) { + if (nodeContents.size() == 1) { + return Pair.of(nodeContents.iterator().next().getValue(), Collections.emptyList()); + } + + _ContentTriple revisions = getContentRevisions(nodeContents); + Optional> 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 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 baseRv = baseOpt.map(Content::getValue).map(rv -> rv.get(finalI)); + Optional 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, 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> contents) { + Content base = null; + Content left = null; + Content right = null; + + for (Content 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, + Content> { + public _ContentTriple( + Optional> first, + Content second, + Content third) { + super(first, second, third); + } + } +} diff --git a/src/main/java/se/kth/spork/spoon/conflict/IsImplicitHandler.java b/src/main/java/se/kth/spork/spoon/conflict/IsImplicitHandler.java new file mode 100644 index 00000000..486bb6b5 --- /dev/null +++ b/src/main/java/se/kth/spork/spoon/conflict/IsImplicitHandler.java @@ -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, Boolean> handleConflict( + Optional baseVal, + Object leftVal, + Object rightVal, + Optional 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); + } + } +} diff --git a/src/main/java/se/kth/spork/spoon/conflict/IsUpperHandler.java b/src/main/java/se/kth/spork/spoon/conflict/IsUpperHandler.java new file mode 100644 index 00000000..03bcaca8 --- /dev/null +++ b/src/main/java/se/kth/spork/spoon/conflict/IsUpperHandler.java @@ -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 means that IS_UPPER is true, + * and 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, Boolean> handleConflict( + Optional baseVal, + Object leftVal, + Object rightVal, + Optional baseElem, + CtElement leftElem, + CtElement rightElem) { + return Pair.of(mergeIsUpper(baseElem, leftElem, rightElem), false); + } + + private static Optional mergeIsUpper(Optional 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(); + } + +} diff --git a/src/main/java/se/kth/spork/spoon/conflict/ModifierHandler.java b/src/main/java/se/kth/spork/spoon/conflict/ModifierHandler.java new file mode 100644 index 00000000..cf6a2067 --- /dev/null +++ b/src/main/java/se/kth/spork/spoon/conflict/ModifierHandler.java @@ -0,0 +1,145 @@ +package se.kth.spork.spoon.conflict; + +import se.kth.spork.util.Pair; +import se.kth.spork.util.Triple; +import spoon.reflect.declaration.CtElement; +import spoon.reflect.declaration.ModifierKind; +import spoon.reflect.path.CtRole; + +import java.util.Collection; +import java.util.HashSet; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * Conflict handler for modifiers. This handler can partially merge results. + * + * @author Simon Larsén + */ +public class ModifierHandler implements ContentConflictHandler { + + @Override + public CtRole getRole() { + return CtRole.MODIFIER; + } + + @Override + @SuppressWarnings("unchecked") + public Pair, Boolean> handleConflict( + Optional baseVal, + Object leftVal, + Object rightVal, + Optional baseElem, + CtElement leftElem, + CtElement rightElem) { + return ModifierHandler.mergeModifierKinds(baseVal.map(o -> (Set) o), (Set) leftVal, (Set) rightVal); + } + + /** + * Separate modifiers into visibility (public, private, protected), keywords (static, final) and all + * others. + * + * @param modifiers A stream of modifiers. + * @return A triple with visibility in first, keywords in second and other in third. + */ + private static Triple, Set, Set> + categorizeModifiers(Stream modifiers) { + Set visibility = new HashSet<>(); + Set keywords = new HashSet<>(); + Set other = new HashSet<>(); + + modifiers.forEach(mod -> { + switch (mod) { + // visibility + case PRIVATE: + case PUBLIC: + case PROTECTED: + visibility.add(mod); + break; + // keywords + case ABSTRACT: + case FINAL: + keywords.add(mod); + break; + default: + other.add(mod); + break; + } + }); + + return Triple.of(visibility, keywords, other); + } + + /** + * Separate modifiers into visibility (public, private, protected), keywords (static, final) and all + * others. + * + * @param modifiers A collection of modifiers. + * @return A triple with visibility in first, keywords in second and other in third. + */ + public static Triple, Set, Set> + categorizeModifiers(Collection modifiers) { + return categorizeModifiers(modifiers.stream()); + } + + /** + * Extract the visibility modifier(s). + * + * @param modifiers A collection of modifiers. + * @return A possibly empty set of visibility modifiers. + */ + private static Set getVisibility(Collection modifiers) { + return categorizeModifiers(modifiers).first; + } + + /** + * Return a pair (conflict, mergedModifiers). + * If the conflict value is true, there is a conflict in the visibility modifiers, and the merged value + * will always be the left one. + */ + private static Pair, Boolean> + mergeModifierKinds(Optional> base, Set left, Set right) { + Set baseModifiers = base.orElseGet(HashSet::new); + + Stream modifiers = Stream.of(baseModifiers, left, right).flatMap(Set::stream); + Triple, Set, Set> + categorizedMods = categorizeModifiers(modifiers); + + Set baseVis = getVisibility(baseModifiers); + Set leftVis = getVisibility(left); + Set rightVis = getVisibility(right); + + Set visibility = categorizedMods.first; + Set keywords = categorizedMods.second; + Set other = categorizedMods.third; + + if (visibility.size() > 1) { + visibility.removeIf(baseModifiers::contains); + } + + // visibility is the only place where we can have obvious addition conflicts + // TODO further analyze conflicts among other modifiers (e.g. you can't combine static and volatile) + boolean conflict = visibility.size() != 1 || + !leftVis.equals(rightVis) && !leftVis.equals(baseVis) && !rightVis.equals(baseVis); + + if (conflict) { + // use left version on conflict to follow the convention + visibility = leftVis; + } + + Set mods = Stream.of(visibility, keywords, other).flatMap(Set::stream) + .filter(mod -> + // present in both left and right == ALL GOOD + left.contains(mod) && right.contains(mod) || + // respect deletions, if an element is present in only one of left and right, and is + // present in base, then it has been deleted + (left.contains(mod) ^ right.contains(mod)) && !baseModifiers.contains(mod) + ) + .collect(Collectors.toSet()); + + return Pair.of(Optional.of(mods), conflict); + } + +} diff --git a/src/main/java/se/kth/spork/spoon/pcsinterpreter/ContentMerger.java b/src/main/java/se/kth/spork/spoon/pcsinterpreter/ContentMerger.java deleted file mode 100644 index adea22cf..00000000 --- a/src/main/java/se/kth/spork/spoon/pcsinterpreter/ContentMerger.java +++ /dev/null @@ -1,311 +0,0 @@ -package se.kth.spork.spoon.pcsinterpreter; - -import se.kth.spork.base3dm.Content; -import se.kth.spork.spoon.conflict.ContentConflict; -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.LineBasedMerge; -import se.kth.spork.util.Pair; -import se.kth.spork.util.Triple; -import spoon.reflect.declaration.CtElement; -import spoon.reflect.declaration.ModifierKind; -import spoon.reflect.path.CtRole; -import spoon.reflect.reference.CtWildcardReference; - -import java.util.*; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -/** - * A class for dealing with merging of content. - * - * @author Simon Larsén - */ -public class ContentMerger { - - /** - * @param nodeContents The contents associated with this node. - * @return A pair of merged contents and a potentially empty collection of unresolved conflicts. - */ - @SuppressWarnings("unchecked") - static Pair> - mergedContent(Set> nodeContents) { - if (nodeContents.size() == 1) { - return Pair.of(nodeContents.iterator().next().getValue(), Collections.emptyList()); - } - - _ContentTriple revisions = getContentRevisions(nodeContents); - Optional> 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 unresolvedConflicts = new ArrayDeque<>(); - - for (int i = 0; i < leftRoledValues.size(); i++) { - int finalI = i; - RoledValue leftPair = leftRoledValues.get(i); - RoledValue rightPair = rightRoledValues.get(i); - assert leftPair.getRole() == rightPair.getRole(); - - Optional baseValOpt = baseOpt.map(Content::getValue).map(rv -> rv.get(finalI)) - .map(RoledValue::getValue); - - CtRole role = leftPair.getRole(); - Object leftVal = leftPair.getValue(); - Object rightVal = rightPair.getValue(); - - if (leftPair.equals(rightPair)) { - // 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)), - leftPair, - rightPair)); - - - 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 { - // we need to actually work for this merge :( - switch (role) { - case IS_IMPLICIT: - if (baseValOpt.isPresent()) { - merged = Optional.of(!(Boolean) baseValOpt.get()); - } else { - // when in doubt, discard implicitness - merged = Optional.of(false); - } - break; - case MODIFIER: - Pair>> mergePair = mergeModifierKinds( - baseValOpt.map(o -> (Set) o), - (Set) leftVal, - (Set) rightVal); - conflictPresent = mergePair.first; - merged = mergePair.second; - break; - case COMMENT_CONTENT: - merged = mergeComments(baseValOpt.orElse(""), leftVal, rightVal); - break; - case IS_UPPER: - merged = mergeIsUpper( - baseOpt.map(c -> c.getValue().getElement()), - leftRoledValues.getElement(), - rightRoledValues.getElement() - ); - break; - default: - // pass - } - } - - - if (merged.isPresent()) { - mergedRoledValues.set(i, role, merged.get()); - - if (!conflictPresent) - unresolvedConflicts.pop(); - } - } - - return Pair.of(mergedRoledValues, new ArrayList<>(unresolvedConflicts)); - } - - /** - * Separate modifiers into visibility (public, private, protected), keywords (static, final) and all - * others. - * - * @param modifiers A stream of modifiers. - * @return A triple with visibility in first, keywords in second and other in third. - */ - public static Triple, Set, Set> - categorizeModifiers(Stream modifiers) { - Set visibility = new HashSet<>(); - Set keywords = new HashSet<>(); - Set other = new HashSet<>(); - - modifiers.forEach(mod -> { - switch (mod) { - // visibility - case PRIVATE: - case PUBLIC: - case PROTECTED: - visibility.add(mod); - break; - // keywords - case ABSTRACT: - case FINAL: - keywords.add(mod); - break; - default: - other.add(mod); - break; - } - }); - - return Triple.of(visibility, keywords, other); - } - - /** - * Separate modifiers into visibility (public, private, protected), keywords (static, final) and all - * others. - * - * @param modifiers A collection of modifiers. - * @return A triple with visibility in first, keywords in second and other in third. - */ - public static Triple, Set, Set> - categorizeModifiers(Collection modifiers) { - return categorizeModifiers(modifiers.stream()); - } - - /** - * Extract the visibility modifier(s). - * - * @param modifiers A collection of modifiers. - * @return A possibly empty set of visibility modifiers. - */ - public static Set getVisibility(Collection modifiers) { - return categorizeModifiers(modifiers).first; - } - - private static Optional mergeIsUpper(Optional 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(); - } - - private static Optional mergeComments(Object base, Object left, Object right) { - Pair merge = LineBasedMerge.merge(base.toString(), left.toString(), right.toString()); - - if (merge.second > 0) { - return Optional.empty(); - } - return Optional.of(merge.first); - } - - /** - * Return a pair (conflict, mergedModifiers). - * If the conflict value is true, there is a conflict in the visibility modifiers, and the merged value - * will always be the left one. - */ - private static Pair>> - mergeModifierKinds(Optional> base, Set left, Set right) { - Set baseModifiers = base.orElseGet(HashSet::new); - - Stream modifiers = Stream.of(baseModifiers, left, right).flatMap(Set::stream); - Triple, Set, Set> - categorizedMods = categorizeModifiers(modifiers); - - Set baseVis = getVisibility(baseModifiers); - Set leftVis = getVisibility(left); - Set rightVis = getVisibility(right); - - Set visibility = categorizedMods.first; - Set keywords = categorizedMods.second; - Set other = categorizedMods.third; - - if (visibility.size() > 1) { - visibility.removeIf(baseModifiers::contains); - } - - // visibility is the only place where we can have obvious addition conflicts - // TODO further analyze conflicts among other modifiers (e.g. you can't combine static and volatile) - boolean conflict = visibility.size() != 1 || - !leftVis.equals(rightVis) && !leftVis.equals(baseVis) && !rightVis.equals(baseVis); - - if (conflict) { - // use left version on conflict to follow the convention - visibility = leftVis; - } - - Set mods = Stream.of(visibility, keywords, other).flatMap(Set::stream) - .filter(mod -> - // present in both left and right == ALL GOOD - left.contains(mod) && right.contains(mod) || - // respect deletions, if an element is present in only one of left and right, and is - // present in base, then it has been deleted - (left.contains(mod) ^ right.contains(mod)) && !baseModifiers.contains(mod) - ) - .collect(Collectors.toSet()); - - return Pair.of(conflict, Optional.of(mods)); - } - - private static _ContentTriple getContentRevisions(Set> contents) { - Content base = null; - Content left = null; - Content right = null; - - for (Content 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, - Content> { - public _ContentTriple( - Optional> first, - Content second, - Content third) { - super(first, second, third); - } - } -} diff --git a/src/main/java/se/kth/spork/spoon/pcsinterpreter/PcsInterpreter.java b/src/main/java/se/kth/spork/spoon/pcsinterpreter/PcsInterpreter.java index 3e75e196..b3aab36f 100644 --- a/src/main/java/se/kth/spork/spoon/pcsinterpreter/PcsInterpreter.java +++ b/src/main/java/se/kth/spork/spoon/pcsinterpreter/PcsInterpreter.java @@ -1,6 +1,7 @@ package se.kth.spork.spoon.pcsinterpreter; import se.kth.spork.base3dm.ChangeSet; +import se.kth.spork.spoon.conflict.ContentConflictHandler; import se.kth.spork.spoon.conflict.StructuralConflictHandler; import se.kth.spork.spoon.wrappers.RoledValues; import se.kth.spork.spoon.matching.SpoonMapping; @@ -30,7 +31,9 @@ public static Pair fromMergedPcs( ChangeSet delta, SpoonMapping baseLeft, SpoonMapping baseRight, - List structuralConflictHandlers) { + List structuralConflictHandlers, + List contentConflictHandlers + ) { SporkTreeBuilder sporkTreeBuilder = new SporkTreeBuilder(delta, baseLeft, baseRight, structuralConflictHandlers); SporkTree sporkTreeRoot = sporkTreeBuilder.buildTree(); @@ -38,7 +41,7 @@ public static Pair fromMergedPcs( // details Environment oldEnv = sporkTreeRoot.getChildren().get(0).getNode().getElement().getFactory().getEnvironment(); - SpoonTreeBuilder spoonTreeBuilder = new SpoonTreeBuilder(baseLeft, baseRight, oldEnv); + SpoonTreeBuilder spoonTreeBuilder = new SpoonTreeBuilder(baseLeft, baseRight, oldEnv, contentConflictHandlers); CtElement spoonTreeRoot = spoonTreeBuilder.build(sporkTreeRoot); return Pair.of(spoonTreeRoot, sporkTreeBuilder.numStructuralConflicts() + spoonTreeBuilder.numContentConflicts()); diff --git a/src/main/java/se/kth/spork/spoon/pcsinterpreter/SpoonTreeBuilder.java b/src/main/java/se/kth/spork/spoon/pcsinterpreter/SpoonTreeBuilder.java index 964c09c3..75a793d6 100644 --- a/src/main/java/se/kth/spork/spoon/pcsinterpreter/SpoonTreeBuilder.java +++ b/src/main/java/se/kth/spork/spoon/pcsinterpreter/SpoonTreeBuilder.java @@ -4,6 +4,8 @@ import se.kth.spork.base3dm.TdmMerge; import se.kth.spork.spoon.*; import se.kth.spork.spoon.conflict.ContentConflict; +import se.kth.spork.spoon.conflict.ContentConflictHandler; +import se.kth.spork.spoon.conflict.ContentMerger; import se.kth.spork.spoon.conflict.StructuralConflict; import se.kth.spork.spoon.matching.SpoonMapping; import se.kth.spork.spoon.wrappers.NodeFactory; @@ -39,24 +41,28 @@ public class SpoonTreeBuilder { // in a merged tree public static final String POSITION_KEY = "spork_position"; - private SpoonMapping baseLeft; - private SpoonMapping baseRight; + private final SpoonMapping baseLeft; + private final SpoonMapping baseRight; private int numContentConflicts = 0; - private Factory factory; + private final Factory factory; // A mapping from the original node to its copy in the merged tree - private Map nodes; + private final Map nodes; + + private final ContentMerger contentMerger; /** * @param baseLeft The base-to-left tree matching. * @param baseRight The base-to-right tree matching. * @param oldEnv Any environment used in the merge. It's needed to copy some values. + * @param contentConflictHandlers A list of conflict handlers. */ - SpoonTreeBuilder(SpoonMapping baseLeft, SpoonMapping baseRight, Environment oldEnv) { + SpoonTreeBuilder(SpoonMapping baseLeft, SpoonMapping baseRight, Environment oldEnv, List contentConflictHandlers) { nodes = new HashMap<>(); this.baseLeft = baseLeft; this.baseRight = baseRight; + contentMerger = new ContentMerger(contentConflictHandlers); // create a new factory Launcher launcher = new Launcher(); @@ -157,7 +163,7 @@ private CtElement visit(SporkTree sporkParent, SporkTree sporkChild) { mergeTree.putMetadata(SINGLE_REVISION_KEY, sporkChild.getSingleRevision()); } else { Pair> mergedContent = - ContentMerger.mergedContent(sporkChild.getContent()); + contentMerger.mergedContent(sporkChild.getContent()); mergeTree = shallowCopyTree(originalTree, factory); mergedContent.first.forEach(rv -> mergeTree.setValueByRole(rv.getRole(), rv.getValue())); diff --git a/src/main/java/se/kth/spork/spoon/printer/PrinterPreprocessor.java b/src/main/java/se/kth/spork/spoon/printer/PrinterPreprocessor.java index e92df340..52cf9aa8 100644 --- a/src/main/java/se/kth/spork/spoon/printer/PrinterPreprocessor.java +++ b/src/main/java/se/kth/spork/spoon/printer/PrinterPreprocessor.java @@ -2,7 +2,7 @@ import se.kth.spork.exception.ConflictException; import se.kth.spork.spoon.conflict.ContentConflict; -import se.kth.spork.spoon.pcsinterpreter.ContentMerger; +import se.kth.spork.spoon.conflict.ModifierHandler; import se.kth.spork.spoon.wrappers.RoledValue; import se.kth.spork.util.LineBasedMerge; import se.kth.spork.util.Pair; @@ -153,8 +153,8 @@ private void processConflict(ContentConflict conflict, CtElement element) { case MODIFIER: Collection leftMods = (Collection) leftVal; Collection rightMods = (Collection) rightVal; - Set leftVisibilities = ContentMerger.categorizeModifiers(leftMods).first; - Set rightVisibilities = ContentMerger.categorizeModifiers(rightMods).first; + Set leftVisibilities = ModifierHandler.categorizeModifiers(leftMods).first; + Set rightVisibilities = ModifierHandler.categorizeModifiers(rightMods).first; if (leftVisibilities.isEmpty()) { // use the right-hand visibility in actual tree to force something to be printed