diff --git a/src/main/java/se/kth/spork/cli/Cli.java b/src/main/java/se/kth/spork/cli/Cli.java index c134333e..2b868f1e 100644 --- a/src/main/java/se/kth/spork/cli/Cli.java +++ b/src/main/java/se/kth/spork/cli/Cli.java @@ -207,7 +207,7 @@ public static Pair merge( LOGGER.info(() -> "Initiating merge"); Pair merge = - Spoon3dmMerge.merge(baseModule, leftModule, rightModule); + Spoon3dmMerge.INSTANCE.merge(baseModule, leftModule, rightModule); CtModule mergeTree = (CtModule) merge.first; int numConflicts = merge.second; diff --git a/src/main/java/se/kth/spork/spoon/Spoon3dmMerge.java b/src/main/java/se/kth/spork/spoon/Spoon3dmMerge.java deleted file mode 100644 index 1668a890..00000000 --- a/src/main/java/se/kth/spork/spoon/Spoon3dmMerge.java +++ /dev/null @@ -1,360 +0,0 @@ -package se.kth.spork.spoon; - -import com.github.gumtreediff.matchers.Matcher; -import com.github.gumtreediff.matchers.Matchers; -import com.github.gumtreediff.tree.ITree; -import gumtree.spoon.builder.SpoonGumTreeBuilder; -import java.nio.file.Path; -import java.util.*; -import java.util.function.BiFunction; -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.MethodOrderingConflictHandler; -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.conflict.StructuralConflictHandler; -import se.kth.spork.spoon.matching.ClassRepresentativesKt; -import se.kth.spork.spoon.matching.MappingRemover; -import se.kth.spork.spoon.matching.SpoonMapping; -import se.kth.spork.spoon.pcsinterpreter.PcsInterpreter; -import se.kth.spork.spoon.wrappers.NodeFactory; -import se.kth.spork.spoon.wrappers.RoledValues; -import se.kth.spork.spoon.wrappers.SpoonNode; -import se.kth.spork.util.LazyLogger; -import se.kth.spork.util.LineBasedMerge; -import se.kth.spork.util.Pair; -import spoon.reflect.declaration.*; - -/** - * Spoon specialization of the 3DM merge algorithm. - * - * @author Simon Larsén - */ -public class Spoon3dmMerge { - private static final LazyLogger LOGGER = new LazyLogger(Spoon3dmMerge.class); - - static { - System.setProperty("gt.xym.sim", "0.7"); - } - - /** - * Merge the left and right revisions with an AST-based merge. - * - * @param base The base revision. - * @param left The left revision. - * @param right The right revision. - * @return A pair on the form (mergeTree, numConflicts). - */ - public static Pair merge(Path base, Path left, Path right) { - long start = System.nanoTime(); - - // PARSING PHASE - LOGGER.info(() -> "Parsing files to Spoon trees"); - CtModule baseTree = Parser.INSTANCE.parse(base); - CtModule leftTree = Parser.INSTANCE.parse(left); - CtModule rightTree = Parser.INSTANCE.parse(right); - - long end = System.nanoTime(); - double timeDelta = (double) (end - start) / 1e9; - LOGGER.info(() -> "Parsed files in " + timeDelta + " seconds"); - - return merge(baseTree, leftTree, rightTree); - } - - /** - * Merge the left and right revisions. The base revision is used for computing edits, and should - * be the best common ancestor of left and right. - * - * @param base The base revision. - * @param left The left revision. - * @param right The right revision. - * @param baseMatcher Function that returns a matcher for the base-to-left and base-to-right - * matchings. - * @param leftRightMatcher Function that returns a matcher for the left-to-right matching. - * @return A pair on the form (mergeTree, numConflicts). - */ - public static Pair merge( - T base, - T left, - T right, - BiFunction baseMatcher, - BiFunction leftRightMatcher) { - long start = System.nanoTime(); - - // MATCHING PHASE - LOGGER.info(() -> "Converting to GumTree trees"); - ITree baseGumtree = new SpoonGumTreeBuilder().getTree(base); - ITree leftGumtree = new SpoonGumTreeBuilder().getTree(left); - ITree rightGumtree = new SpoonGumTreeBuilder().getTree(right); - - LOGGER.info(() -> "Matching trees with GumTree"); - Matcher baseLeftGumtreeMatch = baseMatcher.apply(baseGumtree, leftGumtree); - Matcher baseRightGumtreeMatch = baseMatcher.apply(baseGumtree, rightGumtree); - Matcher leftRightGumtreeMatch = leftRightMatcher.apply(leftGumtree, rightGumtree); - - LOGGER.info(() -> "Converting GumTree matches to Spoon matches"); - SpoonMapping baseLeft = - SpoonMapping.Companion.fromGumTreeMapping(baseLeftGumtreeMatch.getMappings()); - SpoonMapping baseRight = - SpoonMapping.Companion.fromGumTreeMapping(baseRightGumtreeMatch.getMappings()); - SpoonMapping leftRight = - SpoonMapping.Companion.fromGumTreeMapping(leftRightGumtreeMatch.getMappings()); - - // 3DM PHASE - LOGGER.info(() -> "Mapping nodes to class representatives"); - Map classRepMap = - ClassRepresentativesKt.createClassRepresentativesMapping( - base, left, right, baseLeft, baseRight, leftRight); - - LOGGER.info(() -> "Converting Spoon trees to PCS triples"); - Set> t0 = PcsBuilder.fromSpoon(base, Revision.BASE); - Set> t1 = PcsBuilder.fromSpoon(left, Revision.LEFT); - Set> t2 = PcsBuilder.fromSpoon(right, Revision.RIGHT); - - LOGGER.info(() -> "Computing raw PCS merge"); - ChangeSet delta = - new ChangeSet<>(classRepMap, new ContentResolver(), t0, t1, t2); - ChangeSet t0Star = - new ChangeSet<>(classRepMap, new ContentResolver(), t0); - - LOGGER.info(() -> "Resolving final PCS merge"); - TdmMergeKt.resolveRawMerge(t0Star, delta); - - Set rootConflictingNodes = - StructuralConflict.extractRootConflictingNodes(delta.getStructuralConflicts()); - if (!rootConflictingNodes.isEmpty()) { - LOGGER.info(() -> "Root conflicts detected, restarting merge"); - LOGGER.info(() -> "Removing root conflicting nodes from tree matchings"); - MappingRemover.Companion.removeFromMappings( - rootConflictingNodes, baseLeft, baseRight, leftRight); - - LOGGER.info(() -> "Mapping nodes to class representatives"); - classRepMap = - ClassRepresentativesKt.createClassRepresentativesMapping( - base, left, right, baseLeft, baseRight, leftRight); - - LOGGER.info(() -> "Computing raw PCS merge"); - delta = new ChangeSet<>(classRepMap, new ContentResolver(), t0, t1, t2); - - LOGGER.info(() -> "Resolving final PCS merge"); - TdmMergeKt.resolveRawMerge(t0Star, delta); - } - - // INTERPRETER PHASE - LOGGER.info(() -> "Interpreting resolved PCS merge"); - 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; - int numConflicts = merge.second; - - int metadataElementConflicts = mergeMetadataElements(mergeTree, base, left, right); - - LOGGER.info(() -> "Checking for duplicated members"); - int duplicateMemberConflicts = eliminateDuplicateMembers(mergeTree); - - LOGGER.info(() -> "Merged in " + (double) (System.nanoTime() - start) / 1e9 + " seconds"); - - return Pair.of( - mergeTree, numConflicts + metadataElementConflicts + duplicateMemberConflicts); - } - - /** - * Merge the left and right revisions. The base revision is used for computing edits, and should - * be the best common ancestor of left and right. - * - *

Uses the full GumTree matcher for base-to-left and base-to-right, and the XY matcher for - * left-to-right matchings. - * - * @param base The base revision. - * @param left The left revision. - * @param right The right revision. - * @return A pair on the form (mergeTree, numConflicts). - */ - public static Pair merge(T base, T left, T right) { - return merge(base, left, right, Spoon3dmMerge::matchTrees, Spoon3dmMerge::matchTreesXY); - } - - private static int mergeMetadataElements( - CtElement mergeTree, CtElement base, CtElement left, CtElement right) { - int numConflicts = 0; - - if (base.getMetadata(Parser.IMPORT_STATEMENTS) != null) { - LOGGER.info(() -> "Merging import statements"); - List mergedImports = mergeImportStatements(base, left, right); - mergeTree.putMetadata(Parser.IMPORT_STATEMENTS, mergedImports); - } - - if (base.getMetadata(Parser.COMPILATION_UNIT_COMMENT) != null) { - LOGGER.info(() -> "Merging compilation unit comments"); - Pair cuCommentMerge = mergeCuComments(base, left, right); - numConflicts += cuCommentMerge.second; - mergeTree.putMetadata(Parser.COMPILATION_UNIT_COMMENT, cuCommentMerge.first); - } - - return numConflicts; - } - - private static int eliminateDuplicateMembers(CtElement merge) { - List> types = merge.getElements(e -> true); - int numConflicts = 0; - for (CtType type : types) { - numConflicts += eliminateDuplicateMembers(type); - } - return numConflicts; - } - - private static int eliminateDuplicateMembers(CtType type) { - List members = new ArrayList<>(type.getTypeMembers()); - Map memberMap = new HashMap<>(); - int numConflicts = 0; - - for (CtTypeMember member : members) { - String key; - if (member instanceof CtMethod) { - key = ((CtMethod) member).getSignature(); - } else if (member instanceof CtField) { - key = member.getSimpleName(); - } else if (member instanceof CtType) { - key = ((CtType) member).getQualifiedName(); - } else { - continue; - } - - CtTypeMember duplicate = memberMap.get(key); - if (duplicate == null) { - memberMap.put(key, member); - } else { - LOGGER.info(() -> "Merging duplicated member " + key); - - // need to clear the metadata from these members to be able to re-run the merge - member.descendantIterator().forEachRemaining(NodeFactory::clearNonRevisionMetadata); - duplicate - .descendantIterator() - .forEachRemaining(NodeFactory::clearNonRevisionMetadata); - CtTypeMember dummyBase = (CtTypeMember) member.clone(); - dummyBase.setParent(type); - dummyBase.getDirectChildren().forEach(CtElement::delete); - - // we forcibly set the virtual root as parent, as the real parent of these members - // is outside of the current scope - NodeFactory.clearNonRevisionMetadata(member); - NodeFactory.clearNonRevisionMetadata(duplicate); - NodeFactory.clearNonRevisionMetadata(dummyBase); - NodeFactory.forceWrap(member, NodeFactory.INSTANCE.getVirtualRoot()); - NodeFactory.forceWrap(duplicate, NodeFactory.INSTANCE.getVirtualRoot()); - NodeFactory.forceWrap(dummyBase, NodeFactory.INSTANCE.getVirtualRoot()); - - // use the full gumtree matcher as both base matcher and left-to-right matcher - Pair mergePair = - merge( - dummyBase, - member, - duplicate, - Spoon3dmMerge::matchTrees, - Spoon3dmMerge::matchTrees); - numConflicts += mergePair.second; - CtTypeMember mergedMember = mergePair.first; - - member.delete(); - duplicate.delete(); - - type.addTypeMember(mergedMember); - } - } - - return numConflicts; - } - - /** - * Perform a line-based merge of the compilation unit comments. - * - * @return A pair with the merge and the amount of conflicts. - */ - private static Pair mergeCuComments( - CtElement base, CtElement left, CtElement right) { - String baseComment = getCuComment(base); - String leftComment = getCuComment(left); - String rightComment = getCuComment(right); - return LineBasedMerge.merge(baseComment, leftComment, rightComment); - } - - private static String getCuComment(CtElement mod) { - String comment = (String) mod.getMetadata(Parser.COMPILATION_UNIT_COMMENT); - return comment == null ? "" : comment; - } - - /** - * Merge import statements from base, left and right. Import statements are expected to be - * attached to each tree's root node metadata with the {@link Parser#IMPORT_STATEMENTS} key. - * - *

This method naively merges import statements by respecting additions and deletions from - * both revisions. - * - * @param base The base revision. - * @param left The left revision. - * @param right The right revision. - * @return A merged import list, sorted in lexicographical order. - */ - @SuppressWarnings("unchecked") - private static List mergeImportStatements( - CtElement base, CtElement left, CtElement right) { - Set baseImports = - new HashSet<>((Collection) base.getMetadata(Parser.IMPORT_STATEMENTS)); - Set leftImports = - new HashSet<>((Collection) left.getMetadata(Parser.IMPORT_STATEMENTS)); - Set rightImports = - new HashSet<>((Collection) right.getMetadata(Parser.IMPORT_STATEMENTS)); - Set merge = new HashSet<>(); - - // first create union, this respects all additions - merge.addAll(baseImports); - merge.addAll(leftImports); - merge.addAll(rightImports); - - // now remove all elements that were deleted - Set baseLeftDeletions = new HashSet<>(baseImports); - baseLeftDeletions.removeAll(leftImports); - Set baseRightDeletions = new HashSet<>(baseImports); - baseRightDeletions.removeAll(rightImports); - - merge.removeAll(baseLeftDeletions); - merge.removeAll(baseRightDeletions); - - List ret = new ArrayList<>(merge); - ret.sort(Comparator.comparing(CtImport::toString)); - return ret; - } - - private static Matcher matchTrees(ITree src, ITree dst) { - Matcher matcher = Matchers.getInstance().getMatcher(src, dst); - matcher.match(); - return matcher; - } - - private static Matcher matchTreesXY(ITree src, ITree dst) { - Matcher matcher = Matchers.getInstance().getMatcher("xy", src, dst); - matcher.match(); - return matcher; - } -} diff --git a/src/main/kotlin/se/kth/spork/spoon/Spoon3dmMerge.kt b/src/main/kotlin/se/kth/spork/spoon/Spoon3dmMerge.kt new file mode 100644 index 00000000..316bbed5 --- /dev/null +++ b/src/main/kotlin/se/kth/spork/spoon/Spoon3dmMerge.kt @@ -0,0 +1,349 @@ +package se.kth.spork.spoon + +import com.github.gumtreediff.matchers.Matcher +import com.github.gumtreediff.matchers.Matchers +import com.github.gumtreediff.tree.ITree +import gumtree.spoon.builder.SpoonGumTreeBuilder +import se.kth.spork.base3dm.ChangeSet +import se.kth.spork.base3dm.Revision +import se.kth.spork.base3dm.resolveRawMerge +import se.kth.spork.spoon.Parser.parse +import se.kth.spork.spoon.conflict.CommentContentHandler +import se.kth.spork.spoon.conflict.IsImplicitHandler +import se.kth.spork.spoon.conflict.IsUpperHandler +import se.kth.spork.spoon.conflict.MethodOrderingConflictHandler +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.MappingRemover.Companion.removeFromMappings +import se.kth.spork.spoon.matching.SpoonMapping.Companion.fromGumTreeMapping +import se.kth.spork.spoon.matching.createClassRepresentativesMapping +import se.kth.spork.spoon.pcsinterpreter.PcsInterpreter +import se.kth.spork.spoon.wrappers.NodeFactory +import se.kth.spork.spoon.wrappers.NodeFactory.clearNonRevisionMetadata +import se.kth.spork.spoon.wrappers.NodeFactory.forceWrap +import se.kth.spork.spoon.wrappers.NodeFactory.virtualRoot +import se.kth.spork.util.LazyLogger +import se.kth.spork.util.LineBasedMerge +import se.kth.spork.util.Pair +import spoon.reflect.declaration.CtElement +import spoon.reflect.declaration.CtExecutable +import spoon.reflect.declaration.CtField +import spoon.reflect.declaration.CtImport +import spoon.reflect.declaration.CtModule +import spoon.reflect.declaration.CtType +import spoon.reflect.declaration.CtTypeMember +import java.lang.IllegalStateException +import java.nio.file.Path +import java.util.ArrayList +import java.util.Arrays +import java.util.HashSet + +/** + * Spoon specialization of the 3DM merge algorithm. + * + * @author Simon Larsén + */ +object Spoon3dmMerge { + private val LOGGER = LazyLogger(Spoon3dmMerge::class.java) + + /** + * Merge the left and right revisions with an AST-based merge. + * + * @param base The base revision. + * @param left The left revision. + * @param right The right revision. + * @return A pair on the form (mergeTree, numConflicts). + */ + fun merge(base: Path, left: Path, right: Path): Pair { + val start = System.nanoTime() + + // PARSING PHASE + LOGGER.info { "Parsing files to Spoon trees" } + val baseTree = parse(base) + val leftTree = parse(left) + val rightTree = parse(right) + val end = System.nanoTime() + val timeDelta = (end - start).toDouble() / 1e9 + LOGGER.info { "Parsed files in $timeDelta seconds" } + return merge(baseTree, leftTree, rightTree) + } + + /** + * Merge the left and right revisions. The base revision is used for computing edits, and should + * be the best common ancestor of left and right. + * + * @param base The base revision. + * @param left The left revision. + * @param right The right revision. + * @param baseMatcher Function that returns a matcher for the base-to-left and base-to-right + * matchings. + * @param leftRightMatcher Function that returns a matcher for the left-to-right matching. + * @return A pair on the form (mergeTree, numConflicts). + */ + fun merge( + base: T, + left: T, + right: T, + baseMatcher: (ITree, ITree) -> Matcher, + leftRightMatcher: (ITree, ITree) -> Matcher + ): Pair { + val start = System.nanoTime() + + // MATCHING PHASE + LOGGER.info { "Converting to GumTree trees" } + val baseGumtree = SpoonGumTreeBuilder().getTree(base) + val leftGumtree = SpoonGumTreeBuilder().getTree(left) + val rightGumtree = SpoonGumTreeBuilder().getTree(right) + LOGGER.info { "Matching trees with GumTree" } + val baseLeftGumtreeMatch = baseMatcher(baseGumtree, leftGumtree) + val baseRightGumtreeMatch = baseMatcher(baseGumtree, rightGumtree) + val leftRightGumtreeMatch = leftRightMatcher(leftGumtree, rightGumtree) + LOGGER.info { "Converting GumTree matches to Spoon matches" } + val baseLeft = fromGumTreeMapping(baseLeftGumtreeMatch.mappings) + val baseRight = fromGumTreeMapping(baseRightGumtreeMatch.mappings) + val leftRight = fromGumTreeMapping(leftRightGumtreeMatch.mappings) + + // 3DM PHASE + LOGGER.info { "Mapping nodes to class representatives" } + var classRepMap = createClassRepresentativesMapping( + base, left, right, baseLeft, baseRight, leftRight + ) + LOGGER.info { "Converting Spoon trees to PCS triples" } + val t0 = PcsBuilder.fromSpoon(base, Revision.BASE) + val t1 = PcsBuilder.fromSpoon(left, Revision.LEFT) + val t2 = PcsBuilder.fromSpoon(right, Revision.RIGHT) + LOGGER.info { "Computing raw PCS merge" } + var delta = ChangeSet( + classRepMap, ContentResolver(), t0, t1, t2 + ) + val t0Star = ChangeSet( + classRepMap, ContentResolver(), t0 + ) + LOGGER.info { "Resolving final PCS merge" } + resolveRawMerge(t0Star, delta) + val rootConflictingNodes = StructuralConflict.extractRootConflictingNodes(delta.structuralConflicts) + if (!rootConflictingNodes.isEmpty()) { + LOGGER.info { "Root conflicts detected, restarting merge" } + LOGGER.info { "Removing root conflicting nodes from tree matchings" } + removeFromMappings( + rootConflictingNodes, baseLeft, baseRight, leftRight + ) + LOGGER.info { "Mapping nodes to class representatives" } + classRepMap = createClassRepresentativesMapping( + base, left, right, baseLeft, baseRight, leftRight + ) + LOGGER.info { "Computing raw PCS merge" } + delta = ChangeSet(classRepMap, ContentResolver(), t0, t1, t2) + LOGGER.info { "Resolving final PCS merge" } + resolveRawMerge(t0Star, delta) + } + + // INTERPRETER PHASE + LOGGER.info { "Interpreting resolved PCS merge" } + val structuralConflictHandlers = Arrays.asList( + MethodOrderingConflictHandler(), OptimisticInsertInsertHandler() + ) + val contentConflictHandlers = Arrays.asList( + IsImplicitHandler(), + ModifierHandler(), + IsUpperHandler(), + CommentContentHandler() + ) + val 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 + val mergeTree = merge.first as T + val numConflicts = merge.second + val metadataElementConflicts = mergeMetadataElements(mergeTree, base, left, right) + LOGGER.info { "Checking for duplicated members" } + val duplicateMemberConflicts = eliminateDuplicateMembers(mergeTree) + LOGGER.info { "Merged in " + (System.nanoTime() - start).toDouble() / 1e9 + " seconds" } + return Pair.of( + mergeTree, numConflicts + metadataElementConflicts + duplicateMemberConflicts + ) + } + + /** + * Merge the left and right revisions. The base revision is used for computing edits, and should + * be the best common ancestor of left and right. + * + * + * Uses the full GumTree matcher for base-to-left and base-to-right, and the XY matcher for + * left-to-right matchings. + * + * @param base The base revision. + * @param left The left revision. + * @param right The right revision. + * @return A pair on the form (mergeTree, numConflicts). + */ + fun merge(base: T, left: T, right: T): Pair { + return merge(base, left, right, ::matchTrees, ::matchTreesXY) + } + + private fun mergeMetadataElements( + mergeTree: CtElement, + base: CtElement, + left: CtElement, + right: CtElement + ): Int { + var numConflicts = 0 + if (base.getMetadata(Parser.IMPORT_STATEMENTS) != null) { + LOGGER.info { "Merging import statements" } + val mergedImports = mergeImportStatements(base, left, right) + mergeTree.putMetadata(Parser.IMPORT_STATEMENTS, mergedImports) + } + if (base.getMetadata(Parser.COMPILATION_UNIT_COMMENT) != null) { + LOGGER.info { "Merging compilation unit comments" } + val cuCommentMerge = mergeCuComments(base, left, right) + numConflicts += cuCommentMerge.second + mergeTree.putMetadata(Parser.COMPILATION_UNIT_COMMENT, cuCommentMerge.first) + } + return numConflicts + } + + private fun eliminateDuplicateMembers(merge: CtElement): Int { + val types = merge.getElements { _: CtType<*> -> true } + var numConflicts = 0 + for (type in types) { + numConflicts += eliminateDuplicateMembers(type) + } + return numConflicts + } + + private fun eliminateDuplicateMembers(type: CtType<*>): Int { + val members: List = ArrayList(type.typeMembers) + var numConflicts = 0 + + val getMemberName = { member: CtTypeMember -> + when (member) { + is CtExecutable<*> -> member.signature + is CtField<*> -> member.simpleName + is CtType<*> -> member.qualifiedName + else -> throw IllegalStateException("unknown member type ${member.javaClass}") + } + } + + val duplicates: List> = + members.groupBy(getMemberName).filterValues { it.size == 2 }.values.map { + kotlin.Pair(it[0], it[1]) + } + + for ((left, right) in duplicates) { + LOGGER.info { "Merging duplicated member ${getMemberName(left)}" } + left.descendantIterator().forEachRemaining(NodeFactory::clearNonRevisionMetadata) + + left.descendantIterator().forEachRemaining(NodeFactory::clearNonRevisionMetadata) + right + .descendantIterator() + .forEachRemaining(NodeFactory::clearNonRevisionMetadata) + val dummyBase = left.clone() as CtTypeMember + dummyBase.setParent(type) + dummyBase.directChildren.forEach(CtElement::delete) + + // we forcibly set the virtual root as parent, as the real parent of these members + // is outside of the current scope + clearNonRevisionMetadata(left) + clearNonRevisionMetadata(right) + clearNonRevisionMetadata(dummyBase) + forceWrap(left, virtualRoot) + forceWrap(right, virtualRoot) + forceWrap(dummyBase, virtualRoot) + + // use the full gumtree matcher as both base matcher and left-to-right matcher + val mergePair = merge( + dummyBase, + left, + right, + ::matchTrees, + ::matchTrees + ) + numConflicts += mergePair.second + val mergedMember = mergePair.first + left.delete() + right.delete() + + // badness in the Spoon API: addTypeMember returns a generic type that depends only on the + // static type of the returned expression. So we must store the returned expression and declare + // the type, or Kotlin gets grumpy. + val dontcare: CtType<*> = type.addTypeMember(mergedMember) + } + + return numConflicts + } + + /** + * Perform a line-based merge of the compilation unit comments. + * + * @return A pair with the merge and the amount of conflicts. + */ + private fun mergeCuComments( + base: CtElement, + left: CtElement, + right: CtElement + ): Pair { + val baseComment = getCuComment(base) + val leftComment = getCuComment(left) + val rightComment = getCuComment(right) + return LineBasedMerge.merge(baseComment, leftComment, rightComment) + } + + private fun getCuComment(mod: CtElement): String { + val comment = mod.getMetadata(Parser.COMPILATION_UNIT_COMMENT) as String + return comment ?: "" + } + + /** + * Merge import statements from base, left and right. Import statements are expected to be + * attached to each tree's root node metadata with the [Parser.IMPORT_STATEMENTS] key. + * + * + * This method naively merges import statements by respecting additions and deletions from + * both revisions. + * + * @param base The base revision. + * @param left The left revision. + * @param right The right revision. + * @return A merged import list, sorted in lexicographical order. + */ + private fun mergeImportStatements( + base: CtElement, + left: CtElement, + right: CtElement + ): List { + val baseImports = HashSet(base.getMetadata(Parser.IMPORT_STATEMENTS) as Collection) + val leftImports = HashSet(left.getMetadata(Parser.IMPORT_STATEMENTS) as Collection) + val rightImports = HashSet(right.getMetadata(Parser.IMPORT_STATEMENTS) as Collection) + + // first create union, this respects all additions + val rawMerge = baseImports + leftImports + rightImports + + // now remove all elements that were deleted + val baseLeftDeletions = baseImports - leftImports + val baseRightDeletions = baseImports - rightImports + val merge = rawMerge - baseLeftDeletions - baseRightDeletions + return merge.toList().sortedBy(CtImport::toString) + } + + private fun matchTrees(src: ITree, dst: ITree): Matcher { + val matcher = Matchers.getInstance().getMatcher(src, dst) + matcher.match() + return matcher + } + + private fun matchTreesXY(src: ITree, dst: ITree): Matcher { + val matcher = Matchers.getInstance().getMatcher("xy", src, dst) + matcher.match() + return matcher + } + + init { + System.setProperty("gt.xym.sim", "0.7") + } +} diff --git a/src/test/java/se/kth/spork/cli/CliTest.java b/src/test/java/se/kth/spork/cli/CliTest.java index cdf21fc4..f3452cd4 100644 --- a/src/test/java/se/kth/spork/cli/CliTest.java +++ b/src/test/java/se/kth/spork/cli/CliTest.java @@ -97,7 +97,7 @@ void prettyPrint_shouldContainConflict(Util.TestSources sources) throws IOExcept List expectedConflicts = Util.parseConflicts(sources.expected); Pair merged = - Spoon3dmMerge.merge(sources.base, sources.left, sources.right); + Spoon3dmMerge.INSTANCE.merge(sources.base, sources.left, sources.right); CtModule mergeTree = merged.first; Integer numConflicts = merged.second; @@ -118,7 +118,7 @@ void prettyPrint_shouldParseToExpectedTree_whenConflictHasBeenStrippedOut( CtModule expected = Parser.INSTANCE.parse(Util.keepLeftConflict(sources.expected)); Pair merged = - Spoon3dmMerge.merge(sources.base, sources.left, sources.right); + Spoon3dmMerge.INSTANCE.merge(sources.base, sources.left, sources.right); CtModule mergeTree = merged.first; String prettyPrint = Cli.prettyPrint(mergeTree); @@ -136,7 +136,7 @@ void prettyPrint_shouldParseToExpectedTree_whenConflictHasBeenStrippedOut( */ private static void runTestMerge(Util.TestSources sources, Path tempDir) throws IOException { Pair merged = - Spoon3dmMerge.merge(sources.base, sources.left, sources.right); + Spoon3dmMerge.INSTANCE.merge(sources.base, sources.left, sources.right); CtModule mergeTree = merged.first; Object expectedImports = mergeTree.getMetadata(Parser.IMPORT_STATEMENTS); diff --git a/src/test/java/se/kth/spork/spoon/Spoon3dmMergeTest.java b/src/test/java/se/kth/spork/spoon/Spoon3dmMergeTest.java index 0198742d..670d968b 100644 --- a/src/test/java/se/kth/spork/spoon/Spoon3dmMergeTest.java +++ b/src/test/java/se/kth/spork/spoon/Spoon3dmMergeTest.java @@ -37,7 +37,8 @@ void mergeToTree_shouldReturnExpectedTree_whenBothVersionsAreModified(Util.TestS @ArgumentsSource(Util.CleanLineBasedFallbackProvider.class) void merge_shouldBeClean_withGranularLineBasedFallback(Util.TestSources sources) throws IOException { - assertEquals(0, Spoon3dmMerge.merge(sources.base, sources.left, sources.right).second); + assertEquals( + 0, Spoon3dmMerge.INSTANCE.merge(sources.base, sources.left, sources.right).second); } @Disabled @@ -46,7 +47,7 @@ void merge_shouldBeClean_withGranularLineBasedFallback(Util.TestSources sources) void merge_shouldThrow_onUnhandledInconsistencies(Util.TestSources sources) { assertThrows( ConflictException.class, - () -> Spoon3dmMerge.merge(sources.base, sources.left, sources.right)); + () -> Spoon3dmMerge.INSTANCE.merge(sources.base, sources.left, sources.right)); } private static void runTestMerge(Util.TestSources sources) { @@ -57,7 +58,7 @@ private static void runTestMerge(Util.TestSources sources) { assert expectedCuComment != null; Pair merged = - Spoon3dmMerge.merge(sources.base, sources.left, sources.right); + Spoon3dmMerge.INSTANCE.merge(sources.base, sources.left, sources.right); CtModule mergeTree = merged.first; Object mergedImports = mergeTree.getMetadata(Parser.IMPORT_STATEMENTS); Object mergedCuComment = mergeTree.getMetadata(Parser.COMPILATION_UNIT_COMMENT);