Skip to content

Commit

Permalink
[fact] Put content merging into separate class #43
Browse files Browse the repository at this point in the history
  • Loading branch information
slarse committed Mar 16, 2020
1 parent 412d688 commit 46acd59
Show file tree
Hide file tree
Showing 3 changed files with 283 additions and 233 deletions.
278 changes: 278 additions & 0 deletions src/main/java/se/kth/spork/spoon/ContentMerger.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,278 @@
package se.kth.spork.spoon;

import se.kth.spork.base3dm.Content;
import se.kth.spork.base3dm.Pcs;
import se.kth.spork.base3dm.TStar;
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 {

/**
* Try to resolve content conflicts in the merge.
*
* @param delta A merged TStar.
*/
@SuppressWarnings("unchecked")
static void handleContentConflicts(TStar<SpoonNode, RoledValues> delta) {
for (Pcs<SpoonNode> pcs : delta.getStar()) {
SpoonNode pred = pcs.getPredecessor();
Set<Content<SpoonNode, RoledValues>> nodeContents = delta.getContent(pred);

if (nodeContents.size() > 1) {
_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 leftPair = leftRoledValues.get(i);
RoledValue rightPair = rightRoledValues.get(i);
assert leftPair.getRole() == rightPair.getRole();

Optional<Object> 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();

// 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:
merged = mergeModifierKinds(
baseValOpt.map(o -> (Set<ModifierKind>) o),
(Set<ModifierKind>) leftVal,
(Set<ModifierKind>) rightVal);
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());
unresolvedConflicts.pop();
}
}

if (!unresolvedConflicts.isEmpty()) {
// at least one conflict was not resolved
pred.getElement().putMetadata(ContentConflict.METADATA_KEY, new ArrayList<>(unresolvedConflicts));
}

Set<Content<SpoonNode, RoledValues>> contents = new HashSet<>();
contents.add(new Content<>(pcs, mergedRoledValues));
delta.setContent(pred, contents);
}
}
}

/**
* 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<ModifierKind>, Set<ModifierKind>, Set<ModifierKind>>
categorizeModifiers(Stream<ModifierKind> modifiers) {
Set<ModifierKind> visibility = new HashSet<>();
Set<ModifierKind> keywords = new HashSet<>();
Set<ModifierKind> 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);
}

private static Optional<?> 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();
}

private static Optional<?> mergeComments(Object base, Object left, Object right) {
Pair<String, Boolean> merge = LineBasedMerge.merge(base.toString(), left.toString(), right.toString());

if (merge.second) {
return Optional.empty();
}
return Optional.of(merge.first);
}

private static Optional<Set<ModifierKind>>
mergeModifierKinds(Optional<Set<ModifierKind>> base, Set<ModifierKind> left, Set<ModifierKind> right) {
// all revisions must have the same primary value

Set<ModifierKind> baseModifiers = base.orElseGet(HashSet::new);

Stream<ModifierKind> modifiers = Stream.of(baseModifiers, left, right).flatMap(Set::stream);
Triple<Set<ModifierKind>, Set<ModifierKind>, Set<ModifierKind>>
categorizedMods = categorizeModifiers(modifiers);

Set<ModifierKind> visibility = categorizedMods.first;
Set<ModifierKind> keywords = categorizedMods.second;
Set<ModifierKind> 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)
if (visibility.size() != 1) {
return Optional.empty();
}

Set<ModifierKind> 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 Optional.of(mods);
}

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.getContext().getRevision()) {
case BASE:
base = cnt;
break;
case LEFT:
left = cnt;
break;
case RIGHT:
right = cnt;
break;
}
}

if (left == null || right == null)
throw new IllegalStateException("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);
}
}
}
Loading

0 comments on commit 46acd59

Please sign in to comment.