Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add diff3 support #498

Merged
merged 3 commits into from
Oct 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading