diff --git a/src/main/java/org/variantsync/diffdetective/datasets/DatasetDescription.java b/src/main/java/org/variantsync/diffdetective/datasets/DatasetDescription.java index 58cae00d0..3731f6073 100644 --- a/src/main/java/org/variantsync/diffdetective/datasets/DatasetDescription.java +++ b/src/main/java/org/variantsync/diffdetective/datasets/DatasetDescription.java @@ -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 diff --git a/src/main/java/org/variantsync/diffdetective/diff/GitDiffer.java b/src/main/java/org/variantsync/diffdetective/diff/GitDiffer.java index 3c9bdeaf7..0c8588760 100644 --- a/src/main/java/org/variantsync/diffdetective/diff/GitDiffer.java +++ b/src/main/java/org/variantsync/diffdetective/diff/GitDiffer.java @@ -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; @@ -168,6 +169,14 @@ private Yield yieldAllValidIn(final Iterator 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); } @@ -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() + "!"); } diff --git a/src/main/java/org/variantsync/diffdetective/diff/PatchReference.java b/src/main/java/org/variantsync/diffdetective/diff/PatchReference.java new file mode 100644 index 000000000..d3f2c8d3b --- /dev/null +++ b/src/main/java/org/variantsync/diffdetective/diff/PatchReference.java @@ -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 +) { +} diff --git a/src/main/java/org/variantsync/diffdetective/diff/difftree/DiffNode.java b/src/main/java/org/variantsync/diffdetective/diff/difftree/DiffNode.java index 16c7416be..4d16761b1 100644 --- a/src/main/java/org/variantsync/diffdetective/diff/difftree/DiffNode.java +++ b/src/main/java/org/variantsync/diffdetective/diff/difftree/DiffNode.java @@ -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 ); } @@ -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) { @@ -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!"); + } else { + Assert.assertTrue(this.getDirectFeatureMapping() == null, "Node with type " + nodeType + " has a non null feature mapping"); + } } /** diff --git a/src/main/java/org/variantsync/diffdetective/diff/difftree/DiffTree.java b/src/main/java/org/variantsync/diffdetective/diff/difftree/DiffTree.java index ac6778fed..ed27cc7cd 100644 --- a/src/main/java/org/variantsync/diffdetective/diff/difftree/DiffTree.java +++ b/src/main/java/org/variantsync/diffdetective/diff/difftree/DiffTree.java @@ -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; @@ -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> 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 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 diff --git a/src/main/java/org/variantsync/diffdetective/diff/result/DiffError.java b/src/main/java/org/variantsync/diffdetective/diff/result/DiffError.java index 02302d64e..607ac4137 100644 --- a/src/main/java/org/variantsync/diffdetective/diff/result/DiffError.java +++ b/src/main/java/org/variantsync/diffdetective/diff/result/DiffError.java @@ -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. */ diff --git a/src/main/java/org/variantsync/diffdetective/feature/BooleanAbstraction.java b/src/main/java/org/variantsync/diffdetective/feature/BooleanAbstraction.java index 68005d168..f0e5e20e3 100644 --- a/src/main/java/org/variantsync/diffdetective/feature/BooleanAbstraction.java +++ b/src/main/java/org/variantsync/diffdetective/feature/BooleanAbstraction.java @@ -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; /** @@ -20,6 +17,8 @@ private BooleanAbstraction(){} /** Abstraction value for equality checks ==. */ public static final String EQ = "__EQ__"; + /** Abstraction value for inequality checks !=. */ + public static final String NEQ = "__NEQ__"; /** Abstraction value for greater-equals checks >=. */ public static final String GEQ = "__GEQ__"; /** Abstraction value for smaller-equals checks <=. */ @@ -28,7 +27,7 @@ private BooleanAbstraction(){} public static final String GT = "__GT__"; /** Abstraction value for smaller checks <. */ public static final String LT = "__LT__"; - /** Abstraction value for substractions -. */ + /** Abstraction value for subtractions -. */ public static final String SUB = "__SUB__"; /** Abstraction value for additions +. */ public static final String ADD = "__ADD__"; @@ -38,40 +37,114 @@ private BooleanAbstraction(){} public static final String DIV = "__DIV__"; /** Abstraction value for modulo %. */ public static final String MOD = "__MOD__"; + /** Abstraction value for bitwise left shift <<. */ + public static final String LSHIFT = "__LSHIFT__"; + /** Abstraction value for bitwise right shift >>. */ + public static final String RSHIFT = "__RSHIFT__"; + /** Abstraction value for bitwise not ~. */ + public static final String NOT = "__NOT__"; + /** Abstraction value for bitwise and &. */ + public static final String AND = "__AND__"; + /** Abstraction value for bitwise or |. */ + public static final String OR = "__OR__"; + /** Abstraction value for bitwise xor ^. */ + public static final String XOR = "__XOR__"; + /** Abstraction value for the condition of the ternary operator ?. */ + public static final String THEN = "__THEN__"; + /** Abstraction value for the alternative of the ternary operator :. */ + public static final String ELSE = "__ELSE__"; + /** Abstraction value for opening brackets (. */ + public static final String BRACKET_L = "__LB__"; + /** Abstraction value for clsong brackets ). */ + 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 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) + ); + } + + /** + * 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 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 compile(final Map 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 regex_replace) { - for (Map.Entry regex : regex_replace.entrySet()) { - formula = regex.getKey().matcher(formula).replaceAll(regex.getValue()); + private static String abstractAll(String formula, final List replacements) { + for (var replacement : replacements) { + formula = replacement.applyTo(formula); } return formula; } @@ -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__ String old; do { old = formula; diff --git a/src/main/java/org/variantsync/diffdetective/feature/CPPAnnotationParser.java b/src/main/java/org/variantsync/diffdetective/feature/CPPAnnotationParser.java index b36706db7..12aa275e3 100644 --- a/src/main/java/org/variantsync/diffdetective/feature/CPPAnnotationParser.java +++ b/src/main/java/org/variantsync/diffdetective/feature/CPPAnnotationParser.java @@ -40,16 +40,29 @@ public CPPAnnotationParser(final PropositionalFormulaParser formulaParser, CPPDi /** * Parses the condition of the given line of source code that contains a preprocessor macro (i.e., IF, IFDEF, ELIF). * @param line The line of code of a preprocessor annotation. - * @return The formula of the macro in the given line. If no such formula could be parsed, returns a Literal with the line as name. + * @return The formula of the macro in the given line. + * If no such formula could be parsed, returns a Literal with the line's condition as name. * @throws IllFormedAnnotationException when {@link CPPDiffLineFormulaExtractor#extractFormula(String)} throws. */ public Node parseDiffLine(String line) throws IllFormedAnnotationException { - final String formulaStr = extractor.extractFormula(line); - Node formula = formulaParser.parse(formulaStr); + return parseCondition(extractor.extractFormula(line)); + } + + /** + * Parses a condition of a preprocessor macro (i.e., IF, IFDEF, ELIF). + * The given input should not start with preprocessor annotations. + * If the input starts with a preprocessor annotation, use {@link #parseDiffLine} instead. + * The input should have been prepared by {@link CPPDiffLineFormulaExtractor}. + * @param condition The condition of a preprocessor annotation. + * @return The formula of the condition. + * If no such formula could be parsed, returns a Literal with the condition as name. + */ + public Node parseCondition(String condition) { + Node formula = formulaParser.parse(condition); if (formula == null) { // Logger.warn("Could not parse expression '{}' to feature mapping. Using it as literal.", fmString); - formula = new Literal(line); + formula = new Literal(condition); } return formula; diff --git a/src/main/java/org/variantsync/diffdetective/feature/CPPDiffLineFormulaExtractor.java b/src/main/java/org/variantsync/diffdetective/feature/CPPDiffLineFormulaExtractor.java index 5c58bbd67..bd99d9451 100644 --- a/src/main/java/org/variantsync/diffdetective/feature/CPPDiffLineFormulaExtractor.java +++ b/src/main/java/org/variantsync/diffdetective/feature/CPPDiffLineFormulaExtractor.java @@ -2,6 +2,7 @@ import org.variantsync.diffdetective.diff.difftree.parse.IllFormedAnnotationException; +import java.util.function.Supplier; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -17,8 +18,8 @@ public class CPPDiffLineFormulaExtractor { // ^[+-]?\s*#\s*(if|ifdef|ifndef|elif)(\s+(.*)|\((.*)\))$ private static final String CPP_ANNOTATION_REGEX = "^[+-]?\\s*#\\s*(if|ifdef|ifndef|elif)(\\s+(.*)|\\((.*)\\))$"; private static final Pattern CPP_ANNOTATION_REGEX_PATTERN = Pattern.compile(CPP_ANNOTATION_REGEX); - private static final Pattern COMMENT_PATTERN = Pattern.compile("/\\*.*\\*/"); - private static final Pattern DEFINED_PATTERN = Pattern.compile("defined\\(([^)]*)\\)"); + private static final Pattern COMMENT_PATTERN = Pattern.compile("/\\*.*?\\*/"); + private static final Pattern DEFINED_PATTERN = Pattern.compile("\\bdefined\\b(\\s*\\((\\w*)\\))?"); /** * Resolves any macros in the given formula that are relevant for feature annotations. @@ -40,6 +41,8 @@ protected String resolveFeatureMacroFunctions(String formula) { public String extractFormula(final String line) throws IllFormedAnnotationException { // TODO: There still regexes here in replaceAll that could be optimized by precompiling the regexes once. final Matcher matcher = CPP_ANNOTATION_REGEX_PATTERN.matcher(line); + final Supplier couldNotExtractFormula = () -> + IllFormedAnnotationException.IfWithoutCondition("Could not extract formula from line \""+ line + "\"."); String fm; if (matcher.find()) { @@ -49,27 +52,31 @@ public String extractFormula(final String line) throws IllFormedAnnotationExcept fm = matcher.group(4); } } else { - throw IllFormedAnnotationException.IfWithoutCondition("Could not extract formula from line \""+ line + "\"."); + throw couldNotExtractFormula.get(); } // remove comments fm = fm.split("//")[0]; fm = COMMENT_PATTERN.matcher(fm).replaceAll(""); + // remove defined() + fm = DEFINED_PATTERN.matcher(fm).replaceAll("$2"); + // remove whitespace fm = fm.replaceAll("\\s", ""); - // remove defined() - fm = DEFINED_PATTERN.matcher(fm).replaceAll("$1"); - fm = fm.replaceAll("defined ", " "); fm = resolveFeatureMacroFunctions(fm); ////// abstract arithmetics fm = BooleanAbstraction.arithmetics(fm); - fm = BooleanAbstraction.functionCalls(fm); + fm = BooleanAbstraction.parentheses(fm); + + if (fm.isEmpty()) { + throw couldNotExtractFormula.get(); + } // negate for ifndef - if (line.contains("ifndef")) { + if ("ifndef".equals(matcher.group(1))) { fm = "!(" + fm + ")"; } diff --git a/src/test/java/CPPParserTest.java b/src/test/java/CPPParserTest.java new file mode 100644 index 000000000..44b748949 --- /dev/null +++ b/src/test/java/CPPParserTest.java @@ -0,0 +1,109 @@ +import org.variantsync.diffdetective.diff.difftree.parse.IllFormedAnnotationException; +import org.variantsync.diffdetective.feature.CPPDiffLineFormulaExtractor; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.List; + +public class CPPParserTest { + private static record TestCase(String formula, String expected) {} + private static record ThrowingTestCase(String formula) {} + + private static List testCases() { + return List.of( + new TestCase("#if A", "A"), + new TestCase("#ifdef A", "A"), + new TestCase("#ifndef A", "!(A)"), + new TestCase("#elif A", "A"), + + new TestCase("#if !A", "!A"), + new TestCase("#if A && B", "A&&B"), + new TestCase("#if A || B", "A||B"), + new TestCase("#if A && (B || C)", "A&&(B||C)"), + + new TestCase("#if 1 > -42", "1__GT____SUB__42"), + new TestCase("#if 1 > +42", "1__GT____ADD__42"), + new TestCase("#if 42 > A", "42__GT__A"), + new TestCase("#if 42 > ~A", "42__GT____NOT__A"), + new TestCase("#if A + B > 42", "A__ADD__B__GT__42"), + new TestCase("#if A << B", "A__LSHIFT__B"), + new TestCase("#if A ? B : C", "A__THEN__B__ELSE__C"), + new TestCase("#if A >= B && C > D", "A__GEQ__B&&C__GT__D"), + new TestCase("#if A * (B + C)", "A__MUL____LB__B__ADD__C__RB__"), + new TestCase("#if defined(A) && (B * 2) > C", "A&&__LB__B__MUL__2__RB____GT__C"), + + new TestCase("#if A // Comment && B", "A"), + new TestCase("#if A /* Comment */ && B", "A&&B"), + + new TestCase("#if A == B", "A__EQ__B"), + new TestCase("#if A == 1", "A__EQ__1"), + + new TestCase("#if defined A", "A"), + new TestCase("#if defined(A)", "A"), + new TestCase("#if defined (A)", "A"), + new TestCase("#if (defined A)", "__LB__A__RB__"), + new TestCase("#if MACRO (A)", "MACRO__LB__A__RB__"), + new TestCase("#if MACRO (A, B)", "MACRO__LB__A__B__RB__"), + new TestCase("#if MACRO (A, B + C)", "MACRO__LB__A__B__ADD__C__RB__"), + new TestCase("#if MACRO (A, B) == 1", "MACRO__LB__A__B__RB____EQ__1"), + + new TestCase("#if ifndef", "ifndef") + ); + } + + private static List throwingTestCases() { + return List.of( + // Invalid macro + new ThrowingTestCase(""), + new ThrowingTestCase("#"), + new ThrowingTestCase("ifdef A"), + new ThrowingTestCase("#error A"), + new ThrowingTestCase("#iferror A"), + + // Empty formula + new ThrowingTestCase("#ifdef"), + new ThrowingTestCase("#ifdef // Comment"), + new ThrowingTestCase("#ifdef /* Comment */"), + new ThrowingTestCase("#if defined()") + ); + } + + private static List wontfixTestCases() { + return List.of( + new TestCase("#if A == '1'", "A__EQ____TICK__1__TICK__"), + new TestCase("#if A && (B - (C || D))", "A&&(B__MINUS__LB__C__LOR__D__RB__)") + ); + } + + @ParameterizedTest + @MethodSource("testCases") + public void testCase(TestCase testCase) throws IllFormedAnnotationException { + assertEquals( + testCase.expected, + new CPPDiffLineFormulaExtractor().extractFormula(testCase.formula()) + ); + } + + @ParameterizedTest + @MethodSource("throwingTestCases") + public void throwingTestCase(ThrowingTestCase testCase) { + assertThrows(IllFormedAnnotationException.class, () -> + new CPPDiffLineFormulaExtractor().extractFormula(testCase.formula) + ); + } + + @Disabled("WONTFIX") + @ParameterizedTest + @MethodSource("wontfixTestCases") + public void wontfixTestCase(TestCase testCase) throws IllFormedAnnotationException { + assertEquals( + testCase.expected, + new CPPDiffLineFormulaExtractor().extractFormula(testCase.formula()) + ); + } + +}