Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
1bfa44d
fix: remove brackets around single words in boolean abstraction
pmbittner Sep 16, 2022
e621d82
fix: disallow pure whitespace formulas
pmbittner Sep 23, 2022
f9ce4d8
fix: check that if and elif always have a formula upon assertConsiste…
pmbittner Sep 23, 2022
9593879
feat: convencience constructors for commits and patches from repos
pmbittner Sep 23, 2022
6e89cbf
feat: instance of GitPatch that only serves as a reference
pmbittner Sep 23, 2022
30758dc
feat: convenience constructor for DatasetDescription
pmbittner Sep 23, 2022
9533728
feat: test case covering the bug found by TheBormann
pmbittner Sep 23, 2022
f8258dc
Merge branch 'develop' into issue47
pmbittner Oct 6, 2022
b4389b2
Do not accept any empty annotations
ibbem Oct 18, 2022
5e47940
Make `BooleanAbstraction` deterministic
ibbem Oct 18, 2022
391ece1
Add missing operators in BooleanAbstraction
ibbem Oct 18, 2022
e0124a3
Do not get confused by macros with the name "ifndef"
ibbem Oct 18, 2022
2a21846
Correctly replace the `defined` operator
ibbem Oct 18, 2022
36bf3e0
Replace parentheses around already abstracted formulas
ibbem Oct 18, 2022
608c5f4
Do not match C-style comments greedily
ibbem Oct 18, 2022
1d15d54
Check for the absence of feature mappings
ibbem Nov 2, 2022
e69d39b
Don't let `PatchReference` implement `GitPatch`
ibbem Nov 2, 2022
d6e69a5
Fix two resource leaks (`RevWalk` has to be closed)
ibbem Nov 2, 2022
177562c
Move a method for creating a patch from a hash to `GitDiffer`
ibbem Nov 2, 2022
4199b47
Merge branch 'VariationSync/develop' into issue47
ibbem Nov 2, 2022
67abb8e
Fix the annotation parser for unary minus and plus
ibbem Nov 2, 2022
e52b2cd
Replace tests for parsing conditional macros
ibbem Oct 22, 2022
88a4faa
Use `True` as default feature mapping for `DiffNode.fromID`
ibbem Nov 2, 2022
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
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ public record DatasetDescription(
String domain,
String commits
) {
public static DatasetDescription summary(final String name, final String repoURL) {
return new DatasetDescription(name, repoURL, "", "");
}

/**
* Loads all dataset descriptions in the given markdown file.
* This expects the markdown file only be a table with the columns
Expand Down
13 changes: 11 additions & 2 deletions src/main/java/org/variantsync/diffdetective/diff/GitDiffer.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import org.variantsync.diffdetective.diff.result.DiffError;
import org.variantsync.diffdetective.diff.result.DiffParseException;
import org.variantsync.diffdetective.preliminary.GitDiff;
import org.variantsync.diffdetective.util.Assert;
import org.variantsync.diffdetective.util.StringUtils;
import org.variantsync.functjonal.iteration.MappedIterator;
import org.variantsync.functjonal.iteration.SideEffectIterator;
Expand Down Expand Up @@ -168,6 +169,14 @@ private Yield<RevCommit> yieldAllValidIn(final Iterator<RevCommit> commitsIterat
);
}

public CommitDiffResult createCommitDiff(final String commitHash) throws IOException {
Assert.assertNotNull(git);
try (var revWalk = new RevWalk(git.getRepository())) {
final RevCommit commit = revWalk.parseCommit(ObjectId.fromString(commitHash));
return createCommitDiff(commit);
}
}

public CommitDiffResult createCommitDiff(final RevCommit revCommit) {
return createCommitDiffFromFirstParent(git, diffFilter, revCommit, parseOptions);
}
Expand All @@ -193,8 +202,8 @@ public static CommitDiffResult createCommitDiffFromFirstParent(
}

final RevCommit parent;
try {
parent = new RevWalk(git.getRepository()).parseCommit(currentCommit.getParent(0).getId());
try (var revWalk = new RevWalk(git.getRepository())) {
parent = revWalk.parseCommit(currentCommit.getParent(0).getId());
} catch (IOException e) {
return CommitDiffResult.Failure(DiffError.JGIT_ERROR, "Could not parse parent commit of " + currentCommit.getId().getName() + "!");
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package org.variantsync.diffdetective.diff;

/**
* A unique reference to a diff of a file (patch) within an unspecified repository.
*
* @param getFileName the name of the file which was modified
* @param getCommitHash the id of the state after the edit
* @param getParentCommitHash the id of the state before the edit
*
* @author Paul Bittner, Benjamin Moosherr
*/
public record PatchReference(
String getFileName,
String getCommitHash,
String getParentCommitHash
) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -1004,12 +1004,13 @@ public static DiffNode fromID(final int id, String label) {
final int diffTypeOrdinal = (id >> ID_OFFSET) & lowestBitsMask;
final int fromInDiff = (id >> (2*ID_OFFSET)) - 1;

var nodeType = NodeType.values()[nodeTypeOrdinal];
return new DiffNode(
DiffType.values()[diffTypeOrdinal],
NodeType.values()[nodeTypeOrdinal],
nodeType,
new DiffLineNumber(fromInDiff, DiffLineNumber.InvalidLineNumber, DiffLineNumber.InvalidLineNumber),
DiffLineNumber.Invalid(),
null,
nodeType.isConditionalAnnotation() ? FixTrueFalse.True : null,
label
);
}
Expand Down Expand Up @@ -1044,13 +1045,7 @@ public void assertConsistency() {
if (beforeParent != null && afterParent != null) {
Assert.assertTrue(isNon());
}
}

/**
* Checks that Else and Elif nodes have an If or Elif as parent.
* @throws AssertionError when an inconsistency is detected.
*/
public void assertSemanticConsistency() {
// Else and Elif nodes have an If or Elif as parent.
if (this.isElse() || this.isElif()) {
if (beforeParent != null) {
Expand All @@ -1060,6 +1055,13 @@ public void assertSemanticConsistency() {
Assert.assertTrue(afterParent.isIf() || afterParent.isElif(), "After parent " + afterParent + " of " + this + " is neither IF nor ELIF!");
}
}

// Only if and elif nodes have a formula
if (this.isIf() || this.isElif()) {
Assert.assertTrue(this.getDirectFeatureMapping() != null, "If or elif without feature mapping!");
Comment thread
ibbem marked this conversation as resolved.
} else {
Assert.assertTrue(this.getDirectFeatureMapping() == null, "Node with type " + nodeType + " has a non null feature mapping");
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,22 @@
package org.variantsync.diffdetective.diff.difftree;

import org.tinylog.Logger;
import org.variantsync.diffdetective.datasets.Repository;
import org.variantsync.diffdetective.diff.CommitDiff;
import org.variantsync.diffdetective.diff.GitDiffer;
import org.variantsync.diffdetective.diff.PatchDiff;
import org.variantsync.diffdetective.diff.PatchReference;
import org.variantsync.diffdetective.diff.difftree.parse.DiffTreeParser;
import org.variantsync.diffdetective.diff.difftree.source.PatchFile;
import org.variantsync.diffdetective.diff.difftree.source.PatchString;
import org.variantsync.diffdetective.diff.difftree.traverse.DiffTreeTraversal;
import org.variantsync.diffdetective.diff.difftree.traverse.DiffTreeVisitor;
import org.variantsync.diffdetective.diff.result.CommitDiffResult;
import org.variantsync.diffdetective.diff.result.DiffError;
import org.variantsync.diffdetective.diff.result.DiffParseException;
import org.variantsync.diffdetective.feature.CPPAnnotationParser;
import org.variantsync.diffdetective.util.Assert;
import org.variantsync.functjonal.Result;

import java.io.BufferedReader;
import java.io.IOException;
Expand Down Expand Up @@ -114,6 +123,45 @@ public static DiffTree fromDiff(final String diff, boolean collapseMultipleCodeL
return tree;
}

/**
* Parses a patch of a Git repository.
*
* Warning: The current implementation ignored {@code patchReference.getParentCommitHash}.
* It assumes that it's equal to the first parent of {@code patchReference.getCommitHash}, so
* it cannot parse patches across multiple commits.
*
* @param patchReference the patch to be parsed
* @param repository the repository which contains the path {@code patchReference}
* @return a {@link DiffTree} representing the referenced patch, or a list of errors
* encountered while trying to parse the {@link DiffTree}
*/
public static Result<DiffTree, List<DiffError>> fromPatch(final PatchReference patchReference, final Repository repository) throws IOException {
final CommitDiffResult result = new GitDiffer(repository).createCommitDiff(patchReference.getCommitHash());
final Path changedFile = Path.of(patchReference.getFileName());
if (result.diff().isPresent()) {
final CommitDiff commit = result.diff().get();
for (final PatchDiff patch : commit.getPatchDiffs()) {
if (changedFile.equals(Path.of(patch.getFileName()))) {
return Result.Success(patch.getDiffTree());
}
}

Logger.error("There is no patch to "
+ changedFile
+ " in the given commit "
+ patchReference.getCommitHash()
+ " in repo "
+ repository.getRepositoryName()
+ " or it could not be extracted! Reasons are:");

final List<DiffError> errors = new ArrayList<>(result.errors().size() + 1);
errors.add(DiffError.FILE_NOT_FOUND);
errors.addAll(result.errors());
return Result.Failure(errors);
}
return Result.Failure(result.errors());
}

/**
* Invokes the given callback for each node in this DiffTree.
* @param procedure callback
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ public enum DiffError {
*/
JGIT_ERROR("error when operating jgit"),

/**
* The patch of a file was requested, but the file was not found in a git diff.
*/
FILE_NOT_FOUND("couldn't find the requested file or the requested file is unmodified"),

/**
* An error which occurred when obtaining the full diff from a local diff.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
package org.variantsync.diffdetective.feature;

import org.variantsync.functjonal.Functjonal;

import java.util.LinkedHashMap;
import java.util.Map;
import java.util.function.Function;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
Expand All @@ -20,6 +17,8 @@ private BooleanAbstraction(){}

/** Abstraction value for equality checks <code>==</code>. */
public static final String EQ = "__EQ__";
/** Abstraction value for inequality checks <code>!=</code>. */
public static final String NEQ = "__NEQ__";
/** Abstraction value for greater-equals checks <code>&gt;=</code>. */
public static final String GEQ = "__GEQ__";
/** Abstraction value for smaller-equals checks <code>&lt;=</code>. */
Expand All @@ -28,7 +27,7 @@ private BooleanAbstraction(){}
public static final String GT = "__GT__";
/** Abstraction value for smaller checks <code>&lt;</code>. */
public static final String LT = "__LT__";
/** Abstraction value for substractions <code>-</code>. */
/** Abstraction value for subtractions <code>-</code>. */
public static final String SUB = "__SUB__";
/** Abstraction value for additions <code>+</code>. */
public static final String ADD = "__ADD__";
Expand All @@ -38,40 +37,114 @@ private BooleanAbstraction(){}
public static final String DIV = "__DIV__";
/** Abstraction value for modulo <code>%</code>. */
public static final String MOD = "__MOD__";
/** Abstraction value for bitwise left shift <code>&lt;&lt;</code>. */
public static final String LSHIFT = "__LSHIFT__";
/** Abstraction value for bitwise right shift <code>&gt;&gt;</code>. */
public static final String RSHIFT = "__RSHIFT__";
/** Abstraction value for bitwise not <code>~</code>. */
public static final String NOT = "__NOT__";
/** Abstraction value for bitwise and <code>&amp;</code>. */
public static final String AND = "__AND__";
/** Abstraction value for bitwise or <code>|</code>. */
public static final String OR = "__OR__";
/** Abstraction value for bitwise xor <code>^</code>. */
public static final String XOR = "__XOR__";
/** Abstraction value for the condition of the ternary operator <code>?</code>. */
public static final String THEN = "__THEN__";
/** Abstraction value for the alternative of the ternary operator <code>:</code>. */
public static final String ELSE = "__ELSE__";
/** Abstraction value for opening brackets <code>(</code>. */
public static final String BRACKET_L = "__LB__";
/** Abstraction value for clsong brackets <code>)</code>. */
public static final String BRACKET_R = "__RB__";

private static class Replacement {
private Pattern pattern;
private String replacement;

/**
* @param original the literal string to be replaced if it matches a whole word
* @param replacement the replacement with special escape codes according to
* {@link Matcher#replaceAll}
*/
private Replacement(Pattern pattern, String replacement) {
this.pattern = pattern;
this.replacement = replacement;
}

private static final Map<Pattern, String> ARITHMETICS;
static {
ARITHMETICS = compile(Map.of(
"==", EQ,
">=", GEQ,
"<=", LEQ,
">", GT,
"<", LT,
Pattern.quote("+"), ADD,
"-", SUB,
Pattern.quote("*"), MUL,
"/", DIV,
"%", MOD
));
/**
* Creates a new replacement matching {@code original} literally.
*
* @param original a string which is searched for literally (without any special
* characters)
* @param replacement the literal replacement for strings matched by {@code original}
*/
public static Replacement literal(String original, String replacement) {
return new Replacement(
Pattern.compile(Pattern.quote(original)),
Matcher.quoteReplacement(replacement)
);
}

/**
* Creates a new replacement matching {@code original} literally but only on word
* boundaries.
*
* A word boundary is defined as the transition from a word character (alphanumerical
* characters) to a non-word character (everything else) or the transition from any
* character to a bracket (the characters {@code (} and {@code )}).
*
* @param original a string which is searched for as a whole word literally (without any
* special characters)
* @param replacement the literal replacement for strings matched by {@code original}
*/
public static Replacement onlyFullWord(String original, String replacement) {
return new Replacement(
Pattern.compile("(?<=\\b|[()])" + Pattern.quote(original) + "(?=\\b|[()])"),
Matcher.quoteReplacement(replacement)
);
}
Comment thread
ibbem marked this conversation as resolved.

/**
* Replaces all patterns found in {@code value} by its replacement.
*/
public String applyTo(String value) {
return pattern.matcher(value).replaceAll(replacement);
}
}

private static final List<Replacement> ARITHMETICS = List.of(
// These replacements are carefully ordered by their length (longest first) to ensure that
// the longest match is replaced first.
Replacement.literal("<<", LSHIFT),
Replacement.literal(">>", RSHIFT),
Replacement.literal("==", EQ),
Replacement.literal("!=", NEQ),
Replacement.literal(">=", GEQ),
Replacement.literal("<=", LEQ),
Replacement.literal(">", GT),
Replacement.literal("<", LT),
Replacement.literal("+", ADD),
Replacement.literal("-", SUB),
Replacement.literal("*", MUL),
Replacement.literal("/", DIV),
Replacement.literal("%", MOD),
Replacement.literal("^", XOR),
Replacement.literal("~", NOT),
Replacement.literal("?", THEN),
Replacement.literal(":", ELSE),
Replacement.onlyFullWord("&", AND), // && has to be left untouched
Replacement.onlyFullWord("|", OR) // || has to be left untouched
);

private static final Pattern COMMA = Pattern.compile(",");
private static final String COMMA_REPLACEMENT = "__";
private static final Pattern CALL = Pattern.compile("(\\w+)\\((\\w*)\\)");
private static final String CALL_REPLACEMENT = "$1__$2";

private static Map<Pattern, String> compile(final Map<String, String> regex_replace) {
return Functjonal.bimap(
regex_replace,
Pattern::compile,
Function.identity(),
// Use a linked hashmap here to ensure that regexes are always replaced in the same order.
LinkedHashMap::new
);
}
private static final Pattern CALL = Pattern.compile("\\((\\w*)\\)");
private static final String CALL_REPLACEMENT = BRACKET_L + "$1" + BRACKET_R;

private static String abstractAll(String formula, final Map<Pattern, String> regex_replace) {
for (Map.Entry<Pattern, String> regex : regex_replace.entrySet()) {
formula = regex.getKey().matcher(formula).replaceAll(regex.getValue());
private static String abstractAll(String formula, final List<Replacement> replacements) {
for (var replacement : replacements) {
formula = replacement.applyTo(formula);
}
return formula;
}
Expand All @@ -88,23 +161,25 @@ public static String arithmetics(final String formula) {
}

/**
* Abstracts all function calls in the given formula.
* Abstracts parentheses, including the commas of macro calls, in the given formula.
*
* For example, a call "FOO(3, 4, lol)" would be abstracted to a single variable "FOO__3__4__lol".
* The given formula should be a string of a CPP conforming condition.
* @param formula The formula whose function calls should be abstracted.
* @return A copy of the formula with abstracted function calls.
*/
public static String functionCalls(String formula) {
public static String parentheses(String formula) {
////// abstract function calls
/// replace commata in macro calls
formula = COMMA.matcher(formula).replaceAll(COMMA_REPLACEMENT);

/// inline macro calls as long as there are some
/// Example
/// bar(2, foo(baz))
/// -> bar(2__foo(baz)) // because of the comma replacement above
/// -> bar(2__foo__baz)
/// -> bar__2__foo__baz
/// bar(2, foo(A__MUL__(B__PLUS__C))
/// -> bar(2__foo(A__MUL__(B__PLUS__C))) // because of the comma replacement above
/// -> bar(2__foo(A__MUL____LB__B__PLUS__C__RB__))
/// -> bar(2__foo__LB__A__MUL____LB__B__PLUS__C__RB____RB__)
/// -> bar__LB__2__foo__LB__A__MUL____LB__B__PLUS__C__RB____RB____RB__
Comment thread
ibbem marked this conversation as resolved.
String old;
do {
old = formula;
Expand Down
Loading