Skip to content

Commit

Permalink
feat: add diff3 support (#498)
Browse files Browse the repository at this point in the history
  • Loading branch information
wetneb authored Oct 10, 2024
1 parent 5306814 commit 392e743
Show file tree
Hide file tree
Showing 36 changed files with 584 additions and 92 deletions.
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@
<dependency>
<groupId>org.eclipse.jgit</groupId>
<artifactId>org.eclipse.jgit</artifactId>
<version>6.9.0.202403050737-r</version>
<version>6.10.0.202406032230-r</version>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
Expand Down
28 changes: 21 additions & 7 deletions src/main/java/se/kth/spork/cli/Cli.java
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,11 @@ public static String prettyPrint(CtModule spoonRoot) {
.map(Object::toString)
.map(impStmt -> impStmt.substring("import ".length(), impStmt.length() - 1))
.collect(Collectors.toList());
new PrinterPreprocessor(importNames, activePackage.getQualifiedName()).scan(spoonRoot);
new PrinterPreprocessor(
importNames,
activePackage.getQualifiedName(),
Spoon3dmMerge.INSTANCE.getDiff3())
.scan(spoonRoot);

StringBuilder sb = new StringBuilder();

Expand Down Expand Up @@ -135,6 +139,12 @@ static class Merge implements Callable<Integer> {
"Enable Git compatibility mode. Required to use Spork as a Git merge driver.")
boolean gitMode;

@CommandLine.Option(
names = {"--diff3"},
description =
"In conflicts, show the version at the base revision in addition to the left and right versions.")
boolean diff3;

@CommandLine.Option(
names = {"-l", "--logging"},
description = "Enable logging output")
Expand All @@ -145,6 +155,8 @@ public Integer call() throws IOException {
if (logging) {
setLogLevel("DEBUG");
}
Parser.INSTANCE.setDiff3(diff3);
Spoon3dmMerge.INSTANCE.setDiff3(diff3);

long start = System.nanoTime();

Expand All @@ -162,7 +174,7 @@ public Integer call() throws IOException {
rightPath.toFile().deleteOnExit();
}

Pair<String, Integer> merged = merge(basePath, leftPath, rightPath, exitOnError);
Pair<String, Integer> merged = merge(basePath, leftPath, rightPath, exitOnError, diff3);
String pretty = merged.getFirst();
int numConflicts = merged.getSecond();

Expand Down Expand Up @@ -195,10 +207,11 @@ public Integer call() throws IOException {
* @param right Path to right revision.
* @param exitOnError Disallow the use of line-based fallback if the structured merge encounters
* an error.
* @param diff3 whether to use the diff3 style one or the default one
* @return A pair on the form (prettyPrint, numConflicts)
*/
public static Pair<String, Integer> merge(
Path base, Path left, Path right, boolean exitOnError) {
Path base, Path left, Path right, boolean exitOnError, boolean diff3) {
try {
LOGGER.info(() -> "Parsing input files");
CtModule baseModule = Parser.INSTANCE.parse(base);
Expand All @@ -221,7 +234,7 @@ public static Pair<String, Integer> merge(
LOGGER.warn(
() ->
"Merge contains no types (i.e. classes, interfaces, etc), reverting to line-based merge");
return lineBasedMerge(base, left, right);
return lineBasedMerge(base, left, right, diff3);
}
} catch (Exception e) {
if (exitOnError) {
Expand All @@ -234,16 +247,17 @@ public static Pair<String, Integer> merge(
LOGGER.info(
() ->
"Spork encountered an error in structured merge. Falling back to line-based merge");
return lineBasedMerge(base, left, right);
return lineBasedMerge(base, left, right, diff3);
}
}
}

private static Pair<String, Integer> lineBasedMerge(Path base, Path left, Path right) {
private static Pair<String, Integer> lineBasedMerge(
Path base, Path left, Path right, boolean diff3) {
String baseStr = Parser.INSTANCE.read(base);
String leftStr = Parser.INSTANCE.read(left);
String rightStr = Parser.INSTANCE.read(right);
return LineBasedMergeKt.lineBasedMerge(baseStr, leftStr, rightStr);
return LineBasedMergeKt.lineBasedMerge(baseStr, leftStr, rightStr, diff3);
}

/**
Expand Down
43 changes: 31 additions & 12 deletions src/main/java/se/kth/spork/spoon/printer/PrinterPreprocessor.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import java.util.*;
import kotlin.Pair;
import kotlin.Triple;
import se.kth.spork.exception.ConflictException;
import se.kth.spork.spoon.conflict.ContentConflict;
import se.kth.spork.spoon.conflict.ModifierHandler;
Expand Down Expand Up @@ -32,20 +33,22 @@ public class PrinterPreprocessor extends CtScanner {
private final String activePackage;

private final Map<String, Set<CtPackageReference>> refToPack;
private final boolean diff3;

private int currentConflictId;

// A mapping with content_conflict_id -> (left_side, right_side) mappings that are valid
// in the entire source tree
// TODO improve the pretty-printer such that this hack is redundant
private final Map<String, Pair<String, String>> globalContentConflicts;
private final Map<String, Triple<String, String, String>> globalContentConflicts;

public PrinterPreprocessor(List<String> importStatements, String activePackage) {
public PrinterPreprocessor(List<String> importStatements, String activePackage, boolean diff3) {
this.importStatements = importStatements;
this.activePackage = activePackage;
refToPack = new HashMap<>();
currentConflictId = 0;
globalContentConflicts = new HashMap<>();
this.diff3 = diff3;
}

@Override
Expand Down Expand Up @@ -117,13 +120,14 @@ private void handleIncorrectExplicitPackages(CtElement element) {
private void processConflict(ContentConflict conflict, CtElement element) {
Object leftVal = conflict.getLeft().getValue();
Object rightVal = conflict.getRight().getValue();
Object baseVal = conflict.getBase() == null ? "" : conflict.getBase().getValue();

// The local printer map, unlike the global printer map, is only valid in the scope of the
// current CtElement. It contains conflicts for anything that can't be replaced with a
// conflict id,
// such as operators and modifiers (as these are represented by enums)
// TODO improve the pretty-printer such that this hack is redundant
Map<String, Pair<String, String>> localPrinterMap = new HashMap<>();
Map<String, Triple<String, String, String>> localPrinterMap = new HashMap<>();

switch (conflict.getRole()) {
case NAME:
Expand All @@ -132,7 +136,8 @@ private void processConflict(ContentConflict conflict, CtElement element) {
// always scanned separately by the printer (often it just calls `getSimpleName`)
String conflictKey = CONTENT_CONFLICT_PREFIX + currentConflictId++;
globalContentConflicts.put(
conflictKey, new Pair<>(leftVal.toString(), rightVal.toString()));
conflictKey,
new Triple<>(leftVal.toString(), rightVal.toString(), baseVal.toString()));
element.setValueByRole(conflict.getRole(), conflictKey);
break;
case COMMENT_CONTENT:
Expand All @@ -141,60 +146,74 @@ private void processConflict(ContentConflict conflict, CtElement element) {
String rawRight =
(String) conflict.getRight().getMetadata(RoledValue.Key.RAW_CONTENT);
String rawBase =
conflict.getBase() == null
conflict.getBase() != null
? (String)
conflict.getBase().getMetadata(RoledValue.Key.RAW_CONTENT)
: "";

Pair<String, Integer> rawConflict =
LineBasedMergeKt.lineBasedMerge(rawBase, rawLeft, rawRight);
LineBasedMergeKt.lineBasedMerge(rawBase, rawLeft, rawRight, diff3);
assert rawConflict.getSecond() > 0
: "Comments without conflict should already have been merged";

element.putMetadata(RAW_COMMENT_CONFLICT_KEY, rawConflict.getFirst());
break;
case IS_UPPER:
if (leftVal.equals(true)) {
localPrinterMap.put("extends", new Pair<>("extends", "super"));
localPrinterMap.put("extends", new Triple<>("extends", "super", ""));
} else {
localPrinterMap.put("super", new Pair<>("super", "extends"));
localPrinterMap.put("super", new Triple<>("super", "extends", ""));
}
break;
case MODIFIER:
Collection<ModifierKind> leftMods = (Collection<ModifierKind>) leftVal;
Collection<ModifierKind> rightMods = (Collection<ModifierKind>) rightVal;
Collection<ModifierKind> baseMods = (Collection<ModifierKind>) baseVal;
Set<ModifierKind> leftVisibilities =
ModifierHandler.Companion.categorizeModifiers(leftMods).getFirst();
Set<ModifierKind> rightVisibilities =
ModifierHandler.Companion.categorizeModifiers(rightMods).getFirst();

Set<ModifierKind> baseVisibilities =
baseVal == null
? Collections.emptySet()
: ModifierHandler.Companion.categorizeModifiers(baseMods)
.getFirst();

String baseVisStr =
baseVisibilities.isEmpty()
? ""
: baseVisibilities.iterator().next().toString();
if (leftVisibilities.isEmpty()) {
// use the right-hand visibility in actual tree to force something to be printed
Collection<ModifierKind> mods = element.getValueByRole(CtRole.MODIFIER);
ModifierKind rightVis = rightVisibilities.iterator().next();
mods.add(rightVis);
element.setValueByRole(CtRole.MODIFIER, mods);
localPrinterMap.put(rightVis.toString(), new Pair<>("", rightVis.toString()));
localPrinterMap.put(
rightVis.toString(), new Triple<>("", rightVis.toString(), baseVisStr));
} else {
String leftVisStr = leftVisibilities.iterator().next().toString();
String rightVisStr =
rightVisibilities.isEmpty()
? ""
: rightVisibilities.iterator().next().toString();
localPrinterMap.put(leftVisStr, new Pair<>(leftVisStr, rightVisStr));
localPrinterMap.put(
leftVisStr, new Triple<>(leftVisStr, rightVisStr, baseVisStr));
}
break;
case OPERATOR_KIND:
assert leftVal.getClass() == rightVal.getClass();

String leftStr = OperatorHelper.getOperatorText(leftVal);
String rightStr = OperatorHelper.getOperatorText(rightVal);
String baseStr = baseVal == null ? "" : OperatorHelper.getOperatorText(baseVal);

if (element instanceof CtOperatorAssignment) {
leftStr += "=";
rightStr += "=";
baseStr += "=";
}
localPrinterMap.put(leftStr, new Pair<>(leftStr, rightStr));
localPrinterMap.put(leftStr, new Triple<>(leftStr, rightStr, baseStr));
break;
default:
throw new ConflictException("Unhandled conflict: " + leftVal + ", " + rightVal);
Expand Down
49 changes: 32 additions & 17 deletions src/main/java/se/kth/spork/spoon/printer/SporkPrettyPrinter.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package se.kth.spork.spoon.printer;

import java.util.*;
import kotlin.Pair;
import kotlin.Triple;
import se.kth.spork.spoon.conflict.StructuralConflict;
import se.kth.spork.spoon.pcsinterpreter.SpoonTreeBuilder;
import spoon.compiler.Environment;
Expand All @@ -18,20 +18,22 @@

public final class SporkPrettyPrinter extends DefaultJavaPrettyPrinter {
public static final String START_CONFLICT = "<<<<<<< LEFT";
public static final String BASE_CONFLICT = "||||||| BASE";
public static final String MID_CONFLICT = "=======";
public static final String END_CONFLICT = ">>>>>>> RIGHT";

private static final Map<String, Pair<String, String>> DEFAULT_CONFLICT_MAP =
private static final Map<String, Triple<String, String, String>> DEFAULT_CONFLICT_MAP =
Collections.emptyMap();

private final SporkPrinterHelper printerHelper;
private final String lineSeparator = getLineSeparator();
private final boolean diff3;

private Map<String, Pair<String, String>> globalContentConflicts;
private Map<String, Triple<String, String, String>> globalContentConflicts;

private Deque<Optional<Map<String, Pair<String, String>>>> localContentConflictMaps;
private Deque<Optional<Map<String, Triple<String, String, String>>>> localContentConflictMaps;

public SporkPrettyPrinter(Environment env) {
public SporkPrettyPrinter(Environment env, boolean diff3) {
super(env);
printerHelper = new SporkPrinterHelper(env);
localContentConflictMaps = new ArrayDeque<>();
Expand All @@ -48,6 +50,7 @@ public SporkPrettyPrinter(Environment env) {
setIgnoreImplicit(false);

globalContentConflicts = DEFAULT_CONFLICT_MAP;
this.diff3 = diff3;
}

/** Check if the element is a multi declaration (i.e. something like `int a, b, c;`. */
Expand All @@ -72,12 +75,12 @@ private static boolean isMultiDeclaration(CtElement e, String declarationSource)
protected void enter(CtElement e) {
localContentConflictMaps.push(
Optional.ofNullable(
(Map<String, Pair<String, String>>)
(Map<String, Triple<String, String, String>>)
e.getMetadata(PrinterPreprocessor.LOCAL_CONFLICT_MAP_KEY)));

if (globalContentConflicts == DEFAULT_CONFLICT_MAP) {
Map<String, Pair<String, String>> globals =
(Map<String, Pair<String, String>>)
Map<String, Triple<String, String, String>> globals =
(Map<String, Triple<String, String, String>>)
e.getMetadata(PrinterPreprocessor.GLOBAL_CONFLICT_MAP_KEY);
if (globals != null) {
globalContentConflicts = globals;
Expand Down Expand Up @@ -176,7 +179,11 @@ private void handleStructuralConflict(
private void writeStructuralConflict(StructuralConflict structuralConflict) {
String leftSource = SourceExtractor.getOriginalSource(structuralConflict.getLeft());
String rightSource = SourceExtractor.getOriginalSource(structuralConflict.getRight());
printerHelper.writeConflict(leftSource, rightSource);
String baseSource =
structuralConflict.getBase() == null
? ""
: SourceExtractor.getOriginalSource(structuralConflict.getBase());
printerHelper.writeConflict(leftSource, rightSource, baseSource);
}

private class SporkPrinterHelper extends PrinterHelper {
Expand All @@ -186,10 +193,11 @@ public SporkPrinterHelper(Environment env) {

@Override
public SporkPrinterHelper write(String s) {
Optional<Map<String, Pair<String, String>>> localConflictMap =
Optional<Map<String, Triple<String, String, String>>> localConflictMap =
localContentConflictMaps.peek();
String trimmed = s.trim();
if (trimmed.startsWith(START_CONFLICT)
|| trimmed.startsWith(BASE_CONFLICT)
|| trimmed.startsWith(MID_CONFLICT)
|| trimmed.startsWith(END_CONFLICT)) {
// All we need to do here is the decrease tabs and enter some appropriate whitespace
Expand All @@ -199,24 +207,31 @@ public SporkPrinterHelper write(String s) {

String strippedQuotes = trimmed.replaceAll("\"", "");
if (globalContentConflicts.containsKey(strippedQuotes)) {
Pair<String, String> conflict = globalContentConflicts.get(strippedQuotes);
writeConflict(conflict.getFirst(), conflict.getSecond());
Triple<String, String, String> conflict =
globalContentConflicts.get(strippedQuotes);
writeConflict(conflict.getFirst(), conflict.getSecond(), conflict.getThird());
} else if (localConflictMap.isPresent() && localConflictMap.get().containsKey(s)) {
Pair<String, String> conflict = localConflictMap.get().get(s);
writeConflict(conflict.getFirst(), conflict.getSecond());
Triple<String, String, String> conflict = localConflictMap.get().get(s);
writeConflict(conflict.getFirst(), conflict.getSecond(), conflict.getThird());
} else {
super.write(s);
}

return this;
}

public SporkPrinterHelper writeConflict(String left, String right) {
public SporkPrinterHelper writeConflict(String left, String right, String base) {
writelnIfNotPresent()
.writeAtLeftMargin(START_CONFLICT)
.writeln()
.writeAtLeftMargin(left)
.writelnIfNotPresent()
.writeAtLeftMargin(left);
if (diff3) {
writelnIfNotPresent()
.writeAtLeftMargin(BASE_CONFLICT)
.writeln()
.writeAtLeftMargin(base);
}
writelnIfNotPresent()
.writeAtLeftMargin(MID_CONFLICT)
.writeln()
.writeAtLeftMargin(right)
Expand Down
8 changes: 5 additions & 3 deletions src/main/kotlin/se/kth/spork/spoon/Parser.kt
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ object Parser {
const val COMPILATION_UNIT_COMMENT = "spork_cu_comment"
private val LOGGER = LazyLogger(Parser::class.java)

var diff3 = false

/**
* Parse a Java file to a Spoon tree. Any import statements in the file are attached to the returned module's
* metadata with the [Parser.IMPORT_STATEMENTS] key. The imports are sorted in ascending lexicographical
Expand Down Expand Up @@ -55,10 +57,10 @@ object Parser {
}
}

fun setSporkEnvironment(env: Environment, tabulationSize: Int, useTabs: Boolean) {
fun setSporkEnvironment(env: Environment, tabulationSize: Int, useTabs: Boolean, diff3: Boolean) {
env.tabulationSize = tabulationSize
env.useTabulations(useTabs)
env.setPrettyPrinterCreator { SporkPrettyPrinter(env) }
env.setPrettyPrinterCreator { SporkPrettyPrinter(env, diff3) }
env.noClasspath = true
}

Expand All @@ -69,7 +71,7 @@ object Parser {
val indentationGuess = SourceExtractor.guessIndentation(model)
val indentationType = if (indentationGuess.second) "tabs" else "spaces"
LOGGER.info { "Using indentation: " + indentationGuess.first + " " + indentationType }
setSporkEnvironment(launcher.environment, indentationGuess.first, indentationGuess.second)
setSporkEnvironment(launcher.environment, indentationGuess.first, indentationGuess.second, diff3)
val module = model.unnamedModule
module.putMetadata<CtElement>(COMPILATION_UNIT_COMMENT, getCuComment(module))

Expand Down
Loading

0 comments on commit 392e743

Please sign in to comment.