diff --git a/README.md b/README.md index 581845f1c..c3926f447 100644 --- a/README.md +++ b/README.md @@ -44,17 +44,17 @@ Next, build DiffDetective and install it on your system so that you can access i mvn install ``` -To add DiffDetective as a dependency to your own project, add the following snippet to the pom.xml of your Maven project, but make sure to pick the right version number. You can find the version number of DiffDetective at the top of the pom.xml file of DiffDetective. +To add DiffDetective as a dependency to your own project, add the following snippet to the pom.xml of your Maven project, but make sure to pick the right version number. The current version number can be obtained by running `scripts/version.sh` ```xml org.variantsync DiffDetective - 2.1.0 + 2.2.0 ``` -If you prefer to just use a jar file, you can find a jar file with all dependencies at `DiffDetective/target/diffdetective-2.1.0-jar-with-dependencies.jar` (again, the version number might be different). +If you prefer to just use a jar file, you can find a jar file with all dependencies at `DiffDetective/target/diffdetective-2.2.0-jar-with-dependencies.jar` (again, the version number might be different). You can (re-)produce this jar file by either running `mvn package` or `mvn install` within you local clone of DiffDetective. > Disclaimer: Setup tested with maven version 3.6.3. @@ -74,7 +74,7 @@ Afterward, the [result](result) symlink points to the [Javadoc](result/share/git ## How to Get Started -For a demonstration on how to get started using the library, we have prepared a demo repository [here](https://github.com/VariantSync/DiffDetective-Demo). +For a demonstration on how to get started using the library, we have prepared a demo repository [here][demo]. You may clone it as a template and example for including the library into your own projects. Additionally, there is a screencast available on YouTube, guiding you through the demo's setup and source code: @@ -83,13 +83,23 @@ Additionally, there is a screencast available on YouTube, guiding you through th ## Publications +### Variability-Aware Differencing with DiffDetective (FSE 2024) + +[![Preprint](https://img.shields.io/badge/Preprint-Read-purple)](https://github.com/VariantSync/DiffDetective-Demo/blob/raw/Variability-Aware%20Differencing%20with%20DiffDetective.pdf) +[![Screencast](https://img.shields.io/badge/Screencast-Watch-purple)][screencast] +[![Demo Repository](https://img.shields.io/badge/Demo-Try-blue)][demo] + +> P. M. Bittner, A. Schultheiß, B. Moosherr, T. Kehrer, T. Thüm. _Variability-Aware Differencing with DiffDetective_. Demonstrations at International Conference on the Foundations of Software Engineering 2024, ACM, New York, NY, July 2024 + +This paper gives an overview of DiffDetective, its design, features, use-cases, and past case studies. We recommend reading this paper if you are interested in the design of DiffDetective or if you consider using it for your own projects or research. The paper is accompanied by a [demo project][demo] as well as a [screencast][screencast] (see `How to Get Started` above). + ### Classifying Edits to Variability in Source Code (ESEC/FSE 2022) [![Preprint](https://img.shields.io/badge/Preprint-Read-purple)](https://github.com/SoftVarE-Group/Papers/raw/main/2022/2022-ESECFSE-Bittner.pdf) [![Paper](https://img.shields.io/badge/Paper-Read-purple)](https://dl.acm.org/doi/10.1145/3540250.3549108) [![Talk](https://img.shields.io/badge/Talk-Watch-purple)](https://www.youtube.com/watch?v=EnDx1AWxD24) [![Original Replication Package](https://img.shields.io/badge/Replication_Package-Original-blue)](https://github.com/VariantSync/DiffDetective/tree/esecfse22) -[![Updated Replication Package](https://img.shields.io/badge/Replication_Package-Updated-blue)](replication/esecfse22/README.md) +[![Updated Replication Package](https://img.shields.io/badge/Replication_Package-Updated-blue)](replication/esecfse22/) [![Artifact DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.7110095.svg)](https://doi.org/10.5281/zenodo.7110095) > P. M. Bittner, C.Tinnes, A. Schultheiß, S. Viegener, T. Kehrer, T. Thüm. _Classifying Edits to Variability in Source Code_. In Proceedings of the 30th ACM Joint European Software Engineering Conference and Symposium on the Foundations of Software Engineering (ESEC/FSE 2022), ACM, New York, NY, November 2022 @@ -110,7 +120,7 @@ The original replication package can be found on the [esecfse](https://github.co [![Preprint](https://img.shields.io/badge/Preprint-Read-purple)](https://github.com/SoftVarE-Group/Papers/raw/main/2023/2023-SPLC-Bittner.pdf) [![Paper](https://img.shields.io/badge/Paper-Read-purple)](https://dl.acm.org/doi/10.1145/3579027.3608985) [![Original Replication Package](https://img.shields.io/badge/Replication_Package-Original-blue)](https://github.com/VariantSync/DiffDetective/tree/splc23-views/replication/splc23-views) -[![Updated Replication Package](https://img.shields.io/badge/Replication_Package-Updated-blue)](replication/splc23-views/README.md) +[![Updated Replication Package](https://img.shields.io/badge/Replication_Package-Updated-blue)](replication/splc23-views/) [![Artifact DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.8027920.svg)](https://doi.org/10.5281/zenodo.8027920) > P. M. Bittner, A. Schultheiß, S. Greiner, B. Moosherr, S. Krieter, C. Tinnes, T. Kehrer, T. Thüm. _Views on Edits to Variational Software_. In Proceedings of the 27th ACM International Systems and Software Product Line Conference (SPLC 2023), ACM, New York, NY, August 2023 @@ -156,3 +166,5 @@ DiffDetective was extended and used within bachelor's and master's theses: [documentation]: https://variantsync.github.io/DiffDetective/docs/javadoc [website]: https://variantsync.github.io/DiffDetective/ [forklg]: https://github.com/guethilu/DiffDetective +[demo]: https://github.com/VariantSync/DiffDetective-Demo +[screencast]: https://www.youtube.com/watch?v=q6ight5EDQY \ No newline at end of file diff --git a/default.nix b/default.nix index 14ff789a2..235ed586a 100644 --- a/default.nix +++ b/default.nix @@ -10,28 +10,27 @@ doCheck ? true, buildGitHubPages ? true, }: -pkgs.stdenv.mkDerivation rec { +pkgs.stdenvNoCC.mkDerivation rec { pname = "DiffDetective"; - version = "2.1.0"; + # The single source of truth for the version number is stored in `pom.xml`. + # Hence, this XML file needs to be parsed to extract the current version. + version = pkgs.lib.removeSuffix "\n" (pkgs.lib.readFile + (pkgs.runCommandLocal "DiffDetective-version" {} + "${pkgs.xq-xml}/bin/xq -x '/project/version' ${./pom.xml} > $out")); src = with pkgs.lib.fileset; toSource { root = ./.; - # This should be `gitTracked ./.`. However, this currently doesn't accept - # shallow repositories as used in GitHub CI. - fileset = - (import (sources.nixpkgs + "/lib/fileset/internal.nix") {inherit (pkgs) lib;})._fromFetchGit - "gitTracked" - "argument" - ./. - {shallow = true;}; + fileset = gitTracked ./.; }; nativeBuildInputs = with pkgs; [ maven makeWrapper graphviz - (ruby.withPackages (pkgs: with pkgs; [github-pages jekyll-theme-cayman])) - ]; + ] ++ pkgs.lib.optional buildGitHubPages (ruby.withPackages (pkgs: with pkgs; [ + github-pages + jekyll-theme-cayman + ])); mavenRepo = pkgs.stdenv.mkDerivation { pname = "${pname}-mavenRepo"; @@ -60,6 +59,10 @@ pkgs.stdenv.mkDerivation rec { outputHash = "sha256-Gimt6L54yyaX3BtdhQlVu1j4c4y++Mip0GzMl/IfzMc="; }; + jre-minimal = pkgs.callPackage (import "${sources.nixpkgs}/pkgs/development/compilers/openjdk/jre.nix") { + modules = ["java.base" "java.desktop"]; + }; + buildPhase = '' runHook preBuild @@ -74,6 +77,7 @@ pkgs.stdenv.mkDerivation rec { then '' mvn javadoc:javadoc JEKYLL_ENV=production PAGES_REPO_NWO=VariantSync/DiffDetective JEKYLL_BUILD_REVISION= github-pages build + rm -rf _site/target '' else "" } @@ -96,8 +100,8 @@ pkgs.stdenv.mkDerivation rec { local jar="$out/share/java/DiffDetective/DiffDetective.jar" install -Dm644 "target/diffdetective-${version}-jar-with-dependencies.jar" "$jar" - makeWrapper "${pkgs.jdk}/bin/java" "$out/bin/DiffDetective" --add-flags "-cp \"$jar\"" \ - --prefix PATH : "${pkgs.graphviz}" + makeWrapper "${jre-minimal}/bin/java" "$out/bin/DiffDetective" --add-flags "-cp \"$jar\"" \ + --prefix PATH : "${pkgs.graphviz}/bin" ${ if buildGitHubPages diff --git a/nix/sources.json b/nix/sources.json index a22793c4d..94a365575 100644 --- a/nix/sources.json +++ b/nix/sources.json @@ -5,10 +5,10 @@ "homepage": null, "owner": "NixOS", "repo": "nixpkgs", - "rev": "a77ab169a83a4175169d78684ddd2e54486ac651", - "sha256": "0r9a87aqhqr7dkhfy5zrx2dgqq11ma2rfvkfwqhz1xqg7y6mcxxg", + "rev": "6832d0d99649db3d65a0e15fa51471537b2c56a6", + "sha256": "1ww2vrgn8xrznssbd05hdlr3d4br6wbjlqprys1al8ahxkyl5syi", "type": "tarball", - "url": "https://github.com/NixOS/nixpkgs/archive/a77ab169a83a4175169d78684ddd2e54486ac651.tar.gz", + "url": "https://github.com/NixOS/nixpkgs/archive/6832d0d99649db3d65a0e15fa51471537b2c56a6.tar.gz", "url_template": "https://github.com///archive/.tar.gz" } } diff --git a/pom.xml b/pom.xml index c1ce5dce4..53ce2089e 100644 --- a/pom.xml +++ b/pom.xml @@ -6,13 +6,16 @@ org.variantsync diffdetective - 2.1.0 + + 2.2.0 UTF-8 UTF-8 17 17 + + 1 @@ -24,6 +27,7 @@ docs javadoc + private true @@ -94,16 +98,16 @@ org.sat4j core 2.3.5 - - + + de.ovgu featureide.lib.fm 3.8.1 - - + + diff --git a/replication/esecfse22/Dockerfile b/replication/esecfse22/Dockerfile index be3508056..50dc493b7 100644 --- a/replication/esecfse22/Dockerfile +++ b/replication/esecfse22/Dockerfile @@ -32,7 +32,7 @@ WORKDIR /home/sherlock # Copy the compiled JAR file from the first stage into the second stage # Syntax: COPY --from=STAGE_ID SOURCE_PATH TARGET_PATH WORKDIR /home/sherlock/holmes -COPY --from=0 /home/user/target/diffdetective-2.1.0-jar-with-dependencies.jar ./DiffDetective.jar +COPY --from=0 /home/user/target/diffdetective-*-jar-with-dependencies.jar ./DiffDetective.jar WORKDIR /home/sherlock RUN mkdir results diff --git a/replication/splc23-views/Dockerfile b/replication/splc23-views/Dockerfile index 27ced0853..dbb1a24b3 100644 --- a/replication/splc23-views/Dockerfile +++ b/replication/splc23-views/Dockerfile @@ -38,7 +38,7 @@ WORKDIR /home/sherlock # Copy the compiled JAR file from the first stage into the second stage # Syntax: COPY --from=STAGE_ID SOURCE_PATH TARGET_PATH WORKDIR /home/sherlock/holmes -COPY --from=0 /home/user/target/diffdetective-2.1.0-jar-with-dependencies.jar ./DiffDetective.jar +COPY --from=0 /home/user/target/diffdetective-*-jar-with-dependencies.jar ./DiffDetective.jar WORKDIR /home/sherlock # Copy the setup diff --git a/scripts/genUltimateResults.sh b/scripts/genUltimateResults.sh index d15489014..a748d0e49 100755 --- a/scripts/genUltimateResults.sh +++ b/scripts/genUltimateResults.sh @@ -1,4 +1,8 @@ -resultsdir=$1 +#!/usr/bin/env bash -java -cp "target/diffdetective-2.1.0-jar-with-dependencies.jar" org.variantsync.diffdetective.tablegen.MiningResultAccumulator $resultsdir $resultsdir +resultsdir="$1" + +cd "$(dirname "${BASH_SOURCE[0]}")/.." || exit 1 + +java -cp "target/diffdetective-$(./scripts/version.sh)-jar-with-dependencies.jar" org.variantsync.diffdetective.tablegen.MiningResultAccumulator "$resultsdir" "$resultsdir" && echo "genUltimateResults.sh DONE" diff --git a/scripts/runValidation.sh b/scripts/runValidation.sh index 0160f042a..cebd8fcd8 100755 --- a/scripts/runValidation.sh +++ b/scripts/runValidation.sh @@ -1,2 +1,6 @@ -java -cp "target/diffdetective-2.1.0-jar-with-dependencies.jar" org.variantsync.diffdetective.validation.EditClassValidation +#!/usr/bin/env bash + +cd "$(dirname "${BASH_SOURCE[0]}")/.." || exit 1 + +java -cp "target/diffdetective-$(./scripts/version.sh)-jar-with-dependencies.jar" org.variantsync.diffdetective.validation.EditClassValidation && echo "runValidation.sh DONE" diff --git a/scripts/runViewFeasibilityStudy.sh b/scripts/runViewFeasibilityStudy.sh index acb875973..5f2f94d37 100755 --- a/scripts/runViewFeasibilityStudy.sh +++ b/scripts/runViewFeasibilityStudy.sh @@ -1,2 +1,6 @@ -java -cp "target/diffdetective-2.1.0-jar-with-dependencies.jar" org.variantsync.diffdetective.experiments.views.Main "docs/datasets/views.md" +#!/usr/bin/env bash + +cd "$(dirname "${BASH_SOURCE[0]}")/.." || exit 1 + +java -cp "target/diffdetective-$(./scripts/version.sh)-jar-with-dependencies.jar" org.variantsync.diffdetective.experiments.views.Main "docs/datasets/views.md" && echo "runValidation.sh DONE" diff --git a/scripts/version.sh b/scripts/version.sh new file mode 100755 index 000000000..94c51d77a --- /dev/null +++ b/scripts/version.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash + +cd "$(dirname "${BASH_SOURCE[0]}")/.." + +# extracts the first version tag in pom.xml +sed -n '/\(.*\)<\/version.*/\1/; p; q}' pom.xml diff --git a/src/main/antlr4/org/variantsync/diffdetective/feature/antlr/JPPExpression.g4 b/src/main/antlr4/org/variantsync/diffdetective/feature/antlr/JPPExpression.g4 new file mode 100644 index 000000000..ea3d64609 --- /dev/null +++ b/src/main/antlr4/org/variantsync/diffdetective/feature/antlr/JPPExpression.g4 @@ -0,0 +1,320 @@ +grammar JPPExpression; +// A grammar for the JavaPreprocessor +// https://www.slashdev.ca/javapp/ + +expression + : logicalOrExpression + ; + +logicalOrExpression + : logicalAndExpression (OR logicalAndExpression)* + ; + +logicalAndExpression + : primaryExpression (AND primaryExpression)* + ; + +primaryExpression + : definedExpression + | undefinedExpression + | comparisonExpression + ; + +comparisonExpression + : operand ((LT|GT|LEQ|GEQ|EQ|NEQ) operand)? + ; + +operand + : propertyExpression + | unaryOperator Constant + | Constant + | StringLiteral+ + ; + +definedExpression + : 'defined' '(' Identifier ')' + ; + +undefinedExpression + : NOT 'defined' '(' Identifier ')' + ; + +propertyExpression + : '${' Identifier '}' + ; + +unaryOperator + : U_PLUS + | U_MINUS + ; + +U_PLUS : '+'; +U_MINUS : '-'; + +AND : 'and'; +OR : 'or'; +NOT : '!'; + +LT : '<'; +LEQ : '<='; +GT : '>'; +GEQ : '>='; + +EQ : '=='; +NEQ : '!='; + +DOT : '.'; + +Identifier + : IdentifierNondigit ( IdentifierNondigit + | Digit + )* + ; + +fragment +IdentifierNondigit + : Nondigit + | UniversalCharacterName + //| // other implementation-defined characters...y + ; + +fragment +Nondigit + : [a-zA-Z_] + ; + +fragment +Digit + : [0-9] + ; + +fragment +UniversalCharacterName + : '\\u' HexQuad + | '\\U' HexQuad HexQuad + ; + +fragment +HexQuad + : HexadecimalDigit HexadecimalDigit HexadecimalDigit HexadecimalDigit + ; + +Constant + : IntegerConstant + | FloatingConstant + ; + +fragment +IntegerConstant + : DecimalConstant IntegerSuffix? + | OctalConstant IntegerSuffix? + | HexadecimalConstant IntegerSuffix? + | BinaryConstant + ; + +fragment +BinaryConstant + : '0' [bB] [0-1]+ + ; + +fragment +DecimalConstant + : NonzeroDigit Digit* + ; + +fragment +OctalConstant + : '0' OctalDigit* + ; + +fragment +HexadecimalConstant + : HexadecimalPrefix HexadecimalDigit+ + ; + +fragment +HexadecimalPrefix + : '0' [xX] + ; + +fragment +NonzeroDigit + : [1-9] + ; + +fragment +OctalDigit + : [0-7] + ; + +fragment +HexadecimalDigit + : [0-9a-fA-F] + ; + +fragment +IntegerSuffix + : UnsignedSuffix LongSuffix? + | UnsignedSuffix LongLongSuffix + | LongSuffix UnsignedSuffix? + | LongLongSuffix UnsignedSuffix? + ; + +fragment +UnsignedSuffix + : [uU] + ; + +fragment +LongSuffix + : [lL] + ; + +fragment +LongLongSuffix + : 'll' | 'LL' + ; + +fragment +FloatingConstant + : DecimalFloatingConstant + | HexadecimalFloatingConstant + ; + +fragment +DecimalFloatingConstant + : FractionalConstant ExponentPart? FloatingSuffix? + | DigitSequence ExponentPart FloatingSuffix? + ; + +fragment +HexadecimalFloatingConstant + : HexadecimalPrefix (HexadecimalFractionalConstant | HexadecimalDigitSequence) BinaryExponentPart FloatingSuffix? + ; + +fragment +FractionalConstant + : DigitSequence? '.' DigitSequence + | DigitSequence '.' + ; + +fragment +ExponentPart + : [eE] Sign? DigitSequence + ; + +fragment +Sign + : [+-] + ; + +DigitSequence + : Digit+ + ; + +fragment +HexadecimalFractionalConstant + : HexadecimalDigitSequence? '.' HexadecimalDigitSequence + | HexadecimalDigitSequence '.' + ; + +fragment +BinaryExponentPart + : [pP] Sign? DigitSequence + ; + +fragment +HexadecimalDigitSequence + : HexadecimalDigit+ + ; + +fragment +FloatingSuffix + : [flFL] + ; + + +fragment +CCharSequence + : CChar+ + ; + +fragment +CChar + : ~['\\\r\n] + | EscapeSequence + ; + +fragment +EscapeSequence + : SimpleEscapeSequence + | OctalEscapeSequence + | HexadecimalEscapeSequence + | UniversalCharacterName + ; + +fragment +SimpleEscapeSequence + : '\\' ['"?abfnrtv\\] + ; + +fragment +OctalEscapeSequence + : '\\' OctalDigit OctalDigit? OctalDigit? + ; + +fragment +HexadecimalEscapeSequence + : '\\x' HexadecimalDigit+ + ; + +StringLiteral + : EncodingPrefix? '"' SCharSequence? '"' + ; + +fragment +EncodingPrefix + : 'u8' + | 'u' + | 'U' + | 'L' + ; + +fragment +SCharSequence + : SChar+ + ; + +fragment +SChar + : ~["\\\r\n] + | EscapeSequence + | '\\\n' // Added line + | '\\\r\n' // Added line + ; + +Whitespace + : [ \t]+ -> channel(HIDDEN) + ; + +Newline + : ( '\r' '\n'? + | '\n' + ) + -> channel(HIDDEN) + ; + +BlockComment + : '/*' .*? '*/' + -> channel(HIDDEN) + ; + +OpenBlockComment + : '/*' ~[*/]* + -> channel(HIDDEN) + ; + +LineComment + : '//' ~[\r\n]* + -> channel(HIDDEN) + ; \ No newline at end of file diff --git a/src/main/java/org/variantsync/diffdetective/datasets/PatchDiffParseOptions.java b/src/main/java/org/variantsync/diffdetective/datasets/PatchDiffParseOptions.java index d8f7d3d40..10fbe15c7 100644 --- a/src/main/java/org/variantsync/diffdetective/datasets/PatchDiffParseOptions.java +++ b/src/main/java/org/variantsync/diffdetective/datasets/PatchDiffParseOptions.java @@ -1,12 +1,13 @@ package org.variantsync.diffdetective.datasets; -import org.variantsync.diffdetective.feature.CPPAnnotationParser; +import org.variantsync.diffdetective.feature.AnnotationParser; import org.variantsync.diffdetective.variation.diff.parse.VariationDiffParseOptions; /** * Parse options that should be used when parsing commits and patches within a commit history. - * @param diffStoragePolicy Decides if and how unix diffs should be remembered in a parsed - * {@link org.variantsync.diffdetective.diff.git.PatchDiff} when parsing commits. + * + * @param diffStoragePolicy Decides if and how unix diffs should be remembered in a parsed + * {@link org.variantsync.diffdetective.diff.git.PatchDiff} when parsing commits. * @param variationDiffParseOptions Options for parsing a patch to a {@link * org.variantsync.diffdetective.variation.diff.VariationDiff}. For * more information, see {@link VariationDiffParseOptions}. @@ -26,7 +27,7 @@ public enum DiffStoragePolicy { /** * Creates PatchDiffParseOptions with the given annotation parser. */ - public PatchDiffParseOptions withAnnotationParser(CPPAnnotationParser annotationParser) { + public PatchDiffParseOptions withAnnotationParser(AnnotationParser annotationParser) { return new PatchDiffParseOptions( this.diffStoragePolicy(), this.variationDiffParseOptions().withAnnotationParser(annotationParser) diff --git a/src/main/java/org/variantsync/diffdetective/datasets/predefined/Marlin.java b/src/main/java/org/variantsync/diffdetective/datasets/predefined/Marlin.java index 48c777a18..afea92104 100644 --- a/src/main/java/org/variantsync/diffdetective/datasets/predefined/Marlin.java +++ b/src/main/java/org/variantsync/diffdetective/datasets/predefined/Marlin.java @@ -2,7 +2,7 @@ import org.variantsync.diffdetective.datasets.PatchDiffParseOptions; import org.variantsync.diffdetective.datasets.Repository; -import org.variantsync.diffdetective.feature.CPPAnnotationParser; +import org.variantsync.diffdetective.feature.PreprocessorAnnotationParser; import org.variantsync.diffdetective.feature.PropositionalFormulaParser; import java.nio.file.Path; @@ -13,14 +13,15 @@ * @author Kevin Jedelhauser, Paul Maximilian Bittner */ public class Marlin { - public static final CPPAnnotationParser ANNOTATION_PARSER = - new CPPAnnotationParser( - PropositionalFormulaParser.Default, - new MarlinCPPDiffLineFormulaExtractor() - ); + public static final PreprocessorAnnotationParser ANNOTATION_PARSER = + PreprocessorAnnotationParser.CreateCppAnnotationParser( + PropositionalFormulaParser.Default, + new MarlinCPPDiffLineFormulaExtractor() + ); /** * Clones Marlin from Github. + * * @param localDir Directory to clone the repository to. * @return Marlin repository */ diff --git a/src/main/java/org/variantsync/diffdetective/datasets/predefined/MarlinCPPDiffLineFormulaExtractor.java b/src/main/java/org/variantsync/diffdetective/datasets/predefined/MarlinCPPDiffLineFormulaExtractor.java index 15819e3ed..fa1fc3cc0 100644 --- a/src/main/java/org/variantsync/diffdetective/datasets/predefined/MarlinCPPDiffLineFormulaExtractor.java +++ b/src/main/java/org/variantsync/diffdetective/datasets/predefined/MarlinCPPDiffLineFormulaExtractor.java @@ -1,6 +1,6 @@ package org.variantsync.diffdetective.datasets.predefined; -import org.variantsync.diffdetective.feature.CPPDiffLineFormulaExtractor; +import org.variantsync.diffdetective.feature.cpp.CPPDiffLineFormulaExtractor; import java.util.regex.Pattern; @@ -8,6 +8,7 @@ * Extracts formulas from preprocessor annotations in the marlin firmware. * In particular, it resolves the 'ENABLED' and 'DISABLED' macros that are used in Marlin * to check for features being (de-)selected. + * * @author Paul Bittner */ public class MarlinCPPDiffLineFormulaExtractor extends CPPDiffLineFormulaExtractor { @@ -15,11 +16,11 @@ public class MarlinCPPDiffLineFormulaExtractor extends CPPDiffLineFormulaExtract private static final Pattern DISABLED_PATTERN = Pattern.compile("DISABLED\\s*\\(([^)]*)\\)"); @Override - protected String resolveFeatureMacroFunctions(String formula) { + public String resolveFeatureMacroFunctions(String formula) { return - replaceAll(ENABLED_PATTERN, "$1", - replaceAll(DISABLED_PATTERN, "!($1)", - super.resolveFeatureMacroFunctions(formula))); + replaceAll(ENABLED_PATTERN, "$1", + replaceAll(DISABLED_PATTERN, "!($1)", + super.resolveFeatureMacroFunctions(formula))); } private String replaceAll(Pattern pattern, String replacement, String string) { diff --git a/src/main/java/org/variantsync/diffdetective/examplesearch/ExampleFinder.java b/src/main/java/org/variantsync/diffdetective/examplesearch/ExampleFinder.java index f782571d4..3d911c628 100644 --- a/src/main/java/org/variantsync/diffdetective/examplesearch/ExampleFinder.java +++ b/src/main/java/org/variantsync/diffdetective/examplesearch/ExampleFinder.java @@ -7,7 +7,7 @@ import org.variantsync.diffdetective.diff.git.GitPatch; import org.variantsync.diffdetective.diff.result.DiffParseException; import org.variantsync.diffdetective.diff.text.TextBasedDiff; -import org.variantsync.diffdetective.feature.CPPAnnotationParser; +import org.variantsync.diffdetective.feature.AnnotationParser; import org.variantsync.diffdetective.show.Show; import org.variantsync.diffdetective.util.Assert; import org.variantsync.diffdetective.util.IO; @@ -50,9 +50,9 @@ public class ExampleFinder implements Analysis.Hooks { new DefaultEdgeLabelFormat<>(), false, 1000, - RenderOptions.DEFAULT().nodesize()/3, - 0.5*RenderOptions.DEFAULT().edgesize(), - RenderOptions.DEFAULT().arrowsize()/2, + RenderOptions.DEFAULT().nodesize() / 3, + 0.5 * RenderOptions.DEFAULT().edgesize(), + RenderOptions.DEFAULT().arrowsize() / 2, 2, true, List.of() @@ -63,11 +63,12 @@ public class ExampleFinder implements Analysis.Hooks { /** * Creates a new ExampleFinder. + * * @param isGoodExample Function that decides whether a VariationDiff is an example candidate or not. * Should return {@link Optional#empty()} when the given tree is not a good example and thus, should not be considered. * Should return a VariationDiff when the given tree is a good example candidate and should be exported. * The returned VariationDiff might be the exact same VariationDiff or a subtree (e.g., to only export a certain subtree that is relevant). - * @param renderer The renderer to use for rendering example candidates. + * @param renderer The renderer to use for rendering example candidates. */ public ExampleFinder(final ExplainedFilter> isGoodExample, VariationDiffRenderer renderer) { this.isGoodExample = isGoodExample; @@ -101,7 +102,7 @@ private boolean checkIfExample(Analysis analysis, String localDiff) { final Repository currentRepo = analysis.getRepository(); final VariationDiff variationDiff = analysis.getCurrentVariationDiff(); - final CPPAnnotationParser annotationParser = analysis.getRepository().getParseOptions().variationDiffParseOptions().annotationParser(); + final AnnotationParser annotationParser = analysis.getRepository().getParseOptions().variationDiffParseOptions().annotationParser(); // We do not want a variationDiff for the entire file but only for the local change to have a small example. final VariationDiff localTree; diff --git a/src/main/java/org/variantsync/diffdetective/feature/AbstractingCExpressionVisitor.java b/src/main/java/org/variantsync/diffdetective/feature/AbstractingCExpressionVisitor.java deleted file mode 100644 index 415efa312..000000000 --- a/src/main/java/org/variantsync/diffdetective/feature/AbstractingCExpressionVisitor.java +++ /dev/null @@ -1,324 +0,0 @@ -package org.variantsync.diffdetective.feature; -import org.antlr.v4.runtime.ParserRuleContext; -import org.antlr.v4.runtime.tree.AbstractParseTreeVisitor; -import org.antlr.v4.runtime.tree.ParseTree; -import org.antlr.v4.runtime.tree.TerminalNode; -import org.variantsync.diffdetective.feature.antlr.CExpressionParser; -import org.variantsync.diffdetective.feature.antlr.CExpressionVisitor; - -import java.util.function.Function; - -/** - * Visitor that abstracts all symbols of a formula, given as ANTLR parse tree, that might interfere with further formula analysis. - * This visitor traverses the given tree and substitutes all formula substrings with replacements by calling {@link BooleanAbstraction}. - * - *

- * Not all formulas or parts of a formula might require abstraction (e.g., 'A && B'). Therefore, this visitor should not be used directly. - * Instead, you may use a {@link ControllingCExpressionVisitor} which internally uses an {@link AbstractingCExpressionVisitor} - * to control how formulas are abstracted, and only abstracts those parts of a formula that require it. - *

- */ -@SuppressWarnings("CheckReturnValue") -public class AbstractingCExpressionVisitor extends AbstractParseTreeVisitor implements CExpressionVisitor { - - public AbstractingCExpressionVisitor() {} - - // conditionalExpression - // : logicalOrExpression ('?' expression ':' conditionalExpression)? - // ; - @Override public StringBuilder visitConditionalExpression(CExpressionParser.ConditionalExpressionContext ctx) { - return visitExpression(ctx, - childContext -> childContext instanceof CExpressionParser.LogicalOrExpressionContext - || childContext instanceof CExpressionParser.ExpressionContext - || childContext instanceof CExpressionParser.ConditionalExpressionContext); - } - - // primaryExpression - // : macroExpression - // | Identifier - // | Constant - // | StringLiteral+ - // | '(' expression ')' - // | unaryOperator primaryExpression - // | specialOperator - // ; - @Override public StringBuilder visitPrimaryExpression(CExpressionParser.PrimaryExpressionContext ctx) { - // macroExpression - if (ctx.macroExpression() != null) { - return ctx.macroExpression().accept(this); - } - // Identifier - if (ctx.Identifier() != null) { - // Terminal - return new StringBuilder(BooleanAbstraction.abstractAll(ctx.Identifier().getText().trim())); - } - // Constant - if (ctx.Constant() != null) { - // Terminal - return new StringBuilder(BooleanAbstraction.abstractAll(ctx.Constant().getText().trim())); - } - // StringLiteral+ - if (!ctx.StringLiteral().isEmpty()) { - // Terminal - StringBuilder sb = new StringBuilder(); - ctx.StringLiteral().stream().map(ParseTree::getText).map(String::trim).map(BooleanAbstraction::abstractAll).forEach(sb::append); - return sb; - } - // '(' expression ')' - if (ctx.expression() != null) { - StringBuilder sb = ctx.expression().accept(this); - sb.insert(0, BooleanAbstraction.BRACKET_L); - sb.append(BooleanAbstraction.BRACKET_R); - return sb; - } - // unaryOperator primaryExpression - if (ctx.unaryOperator() != null) { - StringBuilder sb = ctx.unaryOperator().accept(this); - sb.append(ctx.primaryExpression().accept(this)); - return sb; - } - // specialOperator - if (ctx.specialOperator() != null) { - return ctx.specialOperator().accept(this); - } - - // Unreachable - throw new IllegalStateException("Unreachable code."); - } - - // unaryOperator - // : '&' | '*' | '+' | '-' | '~' | '!' - // ; - @Override public StringBuilder visitUnaryOperator(CExpressionParser.UnaryOperatorContext ctx) { - if (ctx.And() != null) { - return new StringBuilder(BooleanAbstraction.U_AND); - } - if (ctx.Star() != null) { - return new StringBuilder(BooleanAbstraction.U_STAR); - } - if (ctx.Plus() != null) { - return new StringBuilder(BooleanAbstraction.U_PLUS); - } - if (ctx.Minus() != null) { - return new StringBuilder(BooleanAbstraction.U_MINUS); - } - if (ctx.Tilde() != null) { - return new StringBuilder(BooleanAbstraction.U_TILDE); - } - if (ctx.Not() != null) { - return new StringBuilder(BooleanAbstraction.U_NOT); - } - throw new IllegalStateException(); - } - - // namespaceExpression - // : primaryExpression (':' primaryExpression)* - // ; - @Override - public StringBuilder visitNamespaceExpression(CExpressionParser.NamespaceExpressionContext ctx) { - return visitExpression(ctx, childContext -> childContext instanceof CExpressionParser.PrimaryExpressionContext); - } - - // multiplicativeExpression - // : namespaceExpression (('*'|'/'|'%') namespaceExpression)* - // ; - @Override public StringBuilder visitMultiplicativeExpression(CExpressionParser.MultiplicativeExpressionContext ctx) { - return visitExpression(ctx, childContext -> childContext instanceof CExpressionParser.NamespaceExpressionContext); - } - - // additiveExpression - // : multiplicativeExpression (('+'|'-') multiplicativeExpression)* - // ; - @Override public StringBuilder visitAdditiveExpression(CExpressionParser.AdditiveExpressionContext ctx) { - return visitExpression(ctx, childContext -> childContext instanceof CExpressionParser.MultiplicativeExpressionContext); - } - - // shiftExpression - // : additiveExpression (('<<'|'>>') additiveExpression)* - // ; - @Override public StringBuilder visitShiftExpression(CExpressionParser.ShiftExpressionContext ctx) { - return visitExpression(ctx, childContext -> childContext instanceof CExpressionParser.AdditiveExpressionContext); - } - - // relationalExpression - // : shiftExpression (('<'|'>'|'<='|'>=') shiftExpression)* - // ; - @Override public StringBuilder visitRelationalExpression(CExpressionParser.RelationalExpressionContext ctx) { - return visitExpression(ctx, childContext -> childContext instanceof CExpressionParser.ShiftExpressionContext); - } - - // equalityExpression - // : relationalExpression (('=='| '!=') relationalExpression)* - // ; - @Override public StringBuilder visitEqualityExpression(CExpressionParser.EqualityExpressionContext ctx) { - return visitExpression(ctx, childContext -> childContext instanceof CExpressionParser.RelationalExpressionContext); - } - - // andExpression - // : equalityExpression ( '&' equalityExpression)* - // ; - @Override public StringBuilder visitAndExpression(CExpressionParser.AndExpressionContext ctx) { - return visitExpression(ctx, childContext -> childContext instanceof CExpressionParser.EqualityExpressionContext); - } - - // exclusiveOrExpression - // : andExpression ('^' andExpression)* - // ; - @Override public StringBuilder visitExclusiveOrExpression(CExpressionParser.ExclusiveOrExpressionContext ctx) { - return visitExpression(ctx, childContext -> childContext instanceof CExpressionParser.AndExpressionContext); - } - - // inclusiveOrExpression - // : exclusiveOrExpression ('|' exclusiveOrExpression)* - // ; - @Override public StringBuilder visitInclusiveOrExpression(CExpressionParser.InclusiveOrExpressionContext ctx) { - return visitExpression(ctx, childContext -> childContext instanceof CExpressionParser.ExclusiveOrExpressionContext); - } - - // specialOperator - // : HasAttribute ('(' specialOperatorArgument ')')? - // | HasCPPAttribute ('(' specialOperatorArgument ')')? - // | HasCAttribute ('(' specialOperatorArgument ')')? - // | HasBuiltin ('(' specialOperatorArgument ')')? - // | HasInclude ('(' specialOperatorArgument ')')? - // | Defined ('(' specialOperatorArgument ')') - // | Defined specialOperatorArgument? - // ; - @Override public StringBuilder visitSpecialOperator(CExpressionParser.SpecialOperatorContext ctx) { - return visitExpression(ctx, childContext -> childContext instanceof CExpressionParser.SpecialOperatorArgumentContext); - } - - // specialOperatorArgument - // : HasAttribute - // | HasCPPAttribute - // | HasCAttribute - // | HasBuiltin - // | HasInclude - // | Defined - // | Identifier - // | PathLiteral - // | StringLiteral - // ; - @Override - public StringBuilder visitSpecialOperatorArgument(CExpressionParser.SpecialOperatorArgumentContext ctx) { - return new StringBuilder(BooleanAbstraction.abstractAll(ctx.getText().trim())); - } - - // logicalAndExpression - // : logicalOperand ( '&&' logicalOperand)* - // ; - @Override public StringBuilder visitLogicalAndExpression(CExpressionParser.LogicalAndExpressionContext ctx) { - return visitExpression(ctx, childExpression -> childExpression instanceof CExpressionParser.LogicalOperandContext); - } - - // logicalOrExpression - // : logicalAndExpression ( '||' logicalAndExpression)* - // ; - @Override public StringBuilder visitLogicalOrExpression(CExpressionParser.LogicalOrExpressionContext ctx) { - return visitExpression(ctx, childExpression -> childExpression instanceof CExpressionParser.LogicalAndExpressionContext); - } - - // logicalOperand - // : inclusiveOrExpression - // ; - @Override - public StringBuilder visitLogicalOperand(CExpressionParser.LogicalOperandContext ctx) { - return ctx.inclusiveOrExpression().accept(this); - } - - // macroExpression - // : Identifier '(' argumentExpressionList? ')' - // ; - @Override - public StringBuilder visitMacroExpression(CExpressionParser.MacroExpressionContext ctx) { - StringBuilder sb = new StringBuilder(); - sb.append(ctx.Identifier().getText().trim().toUpperCase()).append("_"); - if (ctx.argumentExpressionList() != null) { - sb.append(ctx.argumentExpressionList().accept(this)); - } - return sb; - } - - // argumentExpressionList - // : assignmentExpression (',' assignmentExpression)* - // | assignmentExpression (assignmentExpression)* - // ; - @Override - public StringBuilder visitArgumentExpressionList(CExpressionParser.ArgumentExpressionListContext ctx) { - StringBuilder sb = new StringBuilder(); - sb.append(BooleanAbstraction.BRACKET_L); - for (int i = 0; i < ctx.assignmentExpression().size(); i++) { - sb.append(ctx.assignmentExpression(i).accept(this)); - if (i < ctx.assignmentExpression().size()-1) { - // For each ',' separating arguments - sb.append("__"); - } - } - sb.append(BooleanAbstraction.BRACKET_R); - return sb; - } - - // assignmentExpression - // : conditionalExpression - // | DigitSequence // for - // | PathLiteral - // | StringLiteral - // | primaryExpression assignmentOperator assignmentExpression - // ; - @Override - public StringBuilder visitAssignmentExpression(CExpressionParser.AssignmentExpressionContext ctx) { - if (ctx.conditionalExpression() != null) { - // conditionalExpression - return ctx.conditionalExpression().accept(this); - } else if (ctx.primaryExpression() != null) { - // primaryExpression assignmentOperator assignmentExpression - StringBuilder sb = new StringBuilder(); - sb.append(ctx.primaryExpression().accept(this)); - sb.append(ctx.assignmentOperator().accept(this)); - sb.append(ctx.assignmentExpression().accept(this)); - return sb; - } else { - // all other cases require direct abstraction - return new StringBuilder(BooleanAbstraction.abstractAll(ctx.getText().trim())); - } - } - - // assignmentOperator - // : '=' | '*=' | '/=' | '%=' | '+=' | '-=' | '<<=' | '>>=' | '&=' | '^=' | '|=' - // ; - @Override - public StringBuilder visitAssignmentOperator(CExpressionParser.AssignmentOperatorContext ctx) { - return new StringBuilder(BooleanAbstraction.abstractToken(ctx.getText().trim())); - } - - // expression - // : assignmentExpression (',' assignmentExpression)* - // ; - @Override - public StringBuilder visitExpression(CExpressionParser.ExpressionContext ctx) { - return visitExpression(ctx, childContext -> childContext instanceof CExpressionParser.AssignmentExpressionContext); - } - - /** - * Abstract all child nodes in the parse tree. - * @param expressionContext The root of the subtree to abstract - * @param instanceCheck A check for expected child node types - * @return The abstracted formula of the subtree - */ - private StringBuilder visitExpression(ParserRuleContext expressionContext, Function instanceCheck) { - StringBuilder sb = new StringBuilder(); - for (ParseTree subtree : expressionContext.children) { - if (instanceCheck.apply(subtree)) { - // Some operand (i.e., a subtree) that we have to visit - sb.append(subtree.accept(this)); - } else if (subtree instanceof TerminalNode terminal) { - // Some operator (i.e., a leaf node) that requires direct abstraction - sb.append(BooleanAbstraction.abstractToken(terminal.getText().trim())); - } else { - // sanity check: loop does not work as expected - throw new IllegalStateException(); - } - } - return sb; - } -} \ No newline at end of file diff --git a/src/main/java/org/variantsync/diffdetective/feature/AbstractingFormulaExtractor.java b/src/main/java/org/variantsync/diffdetective/feature/AbstractingFormulaExtractor.java new file mode 100644 index 000000000..ffe4528cc --- /dev/null +++ b/src/main/java/org/variantsync/diffdetective/feature/AbstractingFormulaExtractor.java @@ -0,0 +1,89 @@ +package org.variantsync.diffdetective.feature; + +import org.tinylog.Logger; +import org.variantsync.diffdetective.error.UncheckedUnParseableFormulaException; +import org.variantsync.diffdetective.error.UnparseableFormulaException; +import org.variantsync.diffdetective.feature.cpp.AbstractingCExpressionVisitor; +import org.variantsync.diffdetective.feature.cpp.ControllingCExpressionVisitor; + +import java.util.function.Supplier; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * AbstractingFormulaExtractor is an abstract class that extracts a formula from text containing a conditional annotation, + * and then abstracts the formula using the custom {@link #abstractFormula(String)} implementation of its subclass. + * The extraction of a formula is controlled by a {@link Pattern} with which an AbstractingFormulaExtractor is initialized. + * The given text might also be a line in a diff (i.e., preceeded by a '-' or '+'). + * + *

+ * For example, given the annotation "#if defined(A) || B()", the extractor should extract the formula + * "defined(A) || B". It would then hand this formula to the {@link #abstractFormula(String)} method for abstraction + * (e.g., to substitute the 'defined(A)' macro call with 'DEFINED_A'). + *

+ * @author Paul Bittner, Sören Viegener, Benjamin Moosherr, Alexander Schultheiß + */ +public abstract class AbstractingFormulaExtractor implements DiffLineFormulaExtractor { + private final Pattern annotationPattern; + + /** + * Initialize a new AbstractingFormulaExtractor object that uses the given Pattern to identify formulas in annotations. + * See {@link org.variantsync.diffdetective.feature.cpp.CPPDiffLineFormulaExtractor} for an example of how such a pattern + * could look like. + * @param annotationPattern The pattern used for formula extraction + */ + public AbstractingFormulaExtractor(Pattern annotationPattern) { + this.annotationPattern = annotationPattern; + } + + /** + * Extracts the feature formula as a string from a piece of text (possibly within a diff) and abstracts it. + * + * @param text The text of which to extract the formula + * @return The extracted and abstracted formula + */ + @Override + public String extractFormula(final String text) throws UnparseableFormulaException { + final Matcher matcher = annotationPattern.matcher(text); + final Supplier couldNotExtractFormula = () -> + new UnparseableFormulaException("Could not extract formula from line \"" + text + "\"."); + + // Retrieve the formula from the macro line + String fm; + if (matcher.find()) { + if (matcher.group(3) != null) { + fm = matcher.group(3); + } else { + fm = matcher.group(4); + } + } else { + throw couldNotExtractFormula.get(); + } + + // abstract complex formulas (e.g., if they contain arithmetics or macro calls) + try { + fm = abstractFormula(fm); + } catch (UncheckedUnParseableFormulaException e) { + throw e.inner(); + } catch (Exception e) { + Logger.warn(e); + throw new UnparseableFormulaException(e); + } + + if (fm.isEmpty()) { + throw couldNotExtractFormula.get(); + } + + return fm; + } + + /** + * Abstract the given formula (e.g., by substituting parts of the formula with predefined String literals). + * See {@link org.variantsync.diffdetective.feature.cpp.CPPDiffLineFormulaExtractor} for an example of how this could + * be done. + * + * @param formula that is to be abstracted + * @return the abstracted formula + */ + protected abstract String abstractFormula(String formula); +} diff --git a/src/main/java/org/variantsync/diffdetective/feature/AnnotationParser.java b/src/main/java/org/variantsync/diffdetective/feature/AnnotationParser.java new file mode 100644 index 000000000..b6c21af67 --- /dev/null +++ b/src/main/java/org/variantsync/diffdetective/feature/AnnotationParser.java @@ -0,0 +1,31 @@ +package org.variantsync.diffdetective.feature; + +import org.prop4j.Node; +import org.variantsync.diffdetective.error.UnparseableFormulaException; + +/** + * Interface for a parser that analyzes annotations in parsed text. The parser is responsible for determining the type + * of the annotation (see {@link AnnotationType}), and parsing the annotation into a {@link Node}. + *

+ * See {@link PreprocessorAnnotationParser} for an example of how an implementation of AnnotationParser could look like. + *

+ */ +public interface AnnotationParser { + /** + * Determine the annotation type for the given piece of text (typically a line of source code). + * + * @param text The text of which the type is determined. + * @return The annotation type of the piece of text. + */ + AnnotationType determineAnnotationType(String text); + + /** + * Parse the condition of the given text containing an annotation (typically a line of source code). + * + * @param text The text containing a conditional annotation + * @return The formula of the condition in the given annotation. + * If no such formula could be extracted, returns a Literal with the line's condition as name. + * @throws UnparseableFormulaException if there is an error while parsing. + */ + Node parseAnnotation(String text) throws UnparseableFormulaException; +} diff --git a/src/main/java/org/variantsync/diffdetective/feature/AnnotationType.java b/src/main/java/org/variantsync/diffdetective/feature/AnnotationType.java new file mode 100644 index 000000000..f66a12bca --- /dev/null +++ b/src/main/java/org/variantsync/diffdetective/feature/AnnotationType.java @@ -0,0 +1,58 @@ +package org.variantsync.diffdetective.feature; + +/** + * AnnotationType is an enum that describes whether a piece of text marks the start of an + * annotation, the end of an annotation, or no annotation at all. + */ +public enum AnnotationType { + /** + * The piece of text (e.g., "#if ...") contains a conditional annotation that starts a new + * annotated subtree in the variation tree. + */ + If("if"), + + /** + * The piece of text (e.g., "#elif ...") contains a conditional annotation which is only checked + * if the conditions of all preceding annotations belonging to the same annotation chain are not fulfilled. + */ + Elif("elif"), + + /** + * The piece of text (e.g., "#else") contains an annotation that marks a subtree as used alternative + * if the condition of the preceding annotation in the same annotation chain is not fulfilled. + */ + Else("else"), + + /** + * The piece of text (e.g., "#endif") marks the end of an annotation (chain). + */ + Endif("endif"), + + /** + * The piece of text contains no annotation. This usually means that it contains an artifact. + */ + None("NONE"); + + public final String name; + + AnnotationType(String name) { + this.name = name; + } + + /** + * Creates a NodeType from its value names. + * + * @param name a string that equals the name of one value of this enum (ignoring case) + * @return The NodeType that has the given name + * @see Enum#name() + */ + public static AnnotationType fromName(final String name) { + for (AnnotationType candidate : values()) { + if (candidate.toString().equalsIgnoreCase(name)) { + return candidate; + } + } + + throw new IllegalArgumentException("Given string \"" + name + "\" is not the name of an AnnotationName."); + } +} diff --git a/src/main/java/org/variantsync/diffdetective/feature/BooleanAbstraction.java b/src/main/java/org/variantsync/diffdetective/feature/BooleanAbstraction.java index 44fe89898..684af5bc2 100644 --- a/src/main/java/org/variantsync/diffdetective/feature/BooleanAbstraction.java +++ b/src/main/java/org/variantsync/diffdetective/feature/BooleanAbstraction.java @@ -1,5 +1,7 @@ package org.variantsync.diffdetective.feature; +import org.variantsync.diffdetective.feature.cpp.AbstractingCExpressionVisitor; + import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -10,122 +12,225 @@ * higher-order logic (e.g., including arithmetics of function calls) * to a propositional formula. * Non-boolean expressions are replaced by respectively named variables. + * * @author Paul Bittner */ public class BooleanAbstraction { - private BooleanAbstraction(){} + 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 >=. */ + /** + * 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 <=. */ + /** + * Abstraction value for smaller-equals checks <=. + */ public static final String LEQ = "__LEQ__"; - /** Abstraction value for greater checks >. */ + /** + * Abstraction value for greater checks >. + */ public static final String GT = "__GT__"; - /** Abstraction value for smaller checks <. */ + /** + * Abstraction value for smaller checks <. + */ public static final String LT = "__LT__"; - /** Abstraction value for subtractions -. */ + /** + * Abstraction value for subtractions -. + */ public static final String SUB = "__SUB__"; - /** Abstraction value for additions +. */ + /** + * Abstraction value for additions +. + */ public static final String ADD = "__ADD__"; - /** Abstraction value for multiplications *. */ + /** + * Abstraction value for multiplications *. + */ public static final String MUL = "__MUL__"; - /** Abstraction value for divisions /. */ + /** + * Abstraction value for divisions /. + */ public static final String DIV = "__DIV__"; - /** Abstraction value for modulo %. */ + /** + * Abstraction value for modulo %. + */ public static final String MOD = "__MOD__"; - /** Abstraction value for bitwise left shift <<. */ + /** + * Abstraction value for bitwise left shift <<. + */ public static final String LSHIFT = "__LSHIFT__"; - /** Abstraction value for bitwise right shift >>. */ + /** + * Abstraction value for bitwise right shift >>. + */ public static final String RSHIFT = "__RSHIFT__"; - /** Abstraction value for bitwise not ~. */ + /** + * Abstraction value for bitwise not ~. + */ public static final String NOT = "__NOT__"; - /** Abstraction value for bitwise and &. */ + /** + * Abstraction value for bitwise and &. + */ public static final String AND = "__AND__"; - /** Abstraction value for bitwise or |. */ + /** + * Abstraction value for bitwise or |. + */ public static final String OR = "__OR__"; - /** Abstraction value for bitwise xor ^. */ + /** + * Abstraction value for bitwise xor ^. + */ public static final String XOR = "__XOR__"; - /** Abstraction value for the condition of the ternary operator ?. */ + /** + * Abstraction value for the condition of the ternary operator ?. + */ public static final String THEN = "__THEN__"; - /** Abstraction value for the alternative of the ternary operator :, or just colons. */ + /** + * Abstraction value for the alternative of the ternary operator :, or just colons. + */ public static final String COLON = "__COLON__"; - /** Abstraction value for opening brackets (. */ + /** + * Abstraction value for opening brackets (. + */ public static final String BRACKET_L = "__LB__"; - /** Abstraction value for closing brackets ). */ + /** + * Abstraction value for closing brackets ). + */ public static final String BRACKET_R = "__RB__"; - /** Abstraction value for unary 'and' &. */ + /** + * Abstraction value for unary 'and' &. + */ public static final String U_AND = "__U_AND__"; - /** Abstraction value for unary star *. */ + /** + * Abstraction value for unary star *. + */ public static final String U_STAR = "__U_STAR__"; - /** Abstraction value for unary plus +. */ + /** + * Abstraction value for unary plus +. + */ public static final String U_PLUS = "__U_PLUS__"; - /** Abstraction value for unary minus -. */ + /** + * Abstraction value for unary minus -. + */ public static final String U_MINUS = "__U_MINUS__"; - /** Abstraction value for unary tilde ~. */ + /** + * Abstraction value for unary tilde ~. + */ public static final String U_TILDE = "__U_TILDE__"; - /** Abstraction value for unary not !. */ + /** + * Abstraction value for unary not !. + */ public static final String U_NOT = "__U_NOT__"; - /** Abstraction value for logical and &&. */ + /** + * Abstraction value for logical and &&. + */ public static final String L_AND = "__L_AND__"; - /** Abstraction value for logical or ||. */ + /** + * Abstraction value for logical or ||. + */ public static final String L_OR = "__L_OR__"; - /** Abstraction value for dots in paths .. */ + /** + * Abstraction value for dots in paths .. + */ public static final String DOT = "__DOT__"; - /** Abstraction value for quotation marks in paths ". */ + /** + * Abstraction value for quotation marks in paths ". + */ public static final String QUOTE = "__QUOTE__"; - /** Abstraction value for single quotation marks '. */ + /** + * Abstraction value for single quotation marks '. + */ public static final String SQUOTE = "__SQUOTE__"; - /** Abstraction value for assign operator =. */ + /** + * Abstraction value for assign operator =. + */ public static final String ASSIGN = "__ASSIGN__"; - /** Abstraction value for star assign operator *=. */ + /** + * Abstraction value for star assign operator *=. + */ public static final String STAR_ASSIGN = "__STA___ASSIGN__"; - /** Abstraction value for div assign operator /=. */ + /** + * Abstraction value for div assign operator /=. + */ public static final String DIV_ASSIGN = "__DIV___ASSIGN__"; - /** Abstraction value for mod assign operator %=. */ + /** + * Abstraction value for mod assign operator %=. + */ public static final String MOD_ASSIGN = "__MOD___ASSIGN__"; - /** Abstraction value for plus assign operator +=. */ + /** + * Abstraction value for plus assign operator +=. + */ public static final String PLUS_ASSIGN = "__PLU___ASSIGN__"; - /** Abstraction value for minus assign operator -=. */ + /** + * Abstraction value for minus assign operator -=. + */ public static final String MINUS_ASSIGN = "__MIN___ASSIGN__"; - /** Abstraction value for left shift assign operator <<=. */ + /** + * Abstraction value for left shift assign operator <<=. + */ public static final String LEFT_SHIFT_ASSIGN = "__LSH___ASSIGN__"; - /** Abstraction value for right shift assign operator >>=. */ + /** + * Abstraction value for right shift assign operator >>=. + */ public static final String RIGHT_SHIFT_ASSIGN = "__RSH___ASSIGN__"; - /** Abstraction value for 'and' assign operator &=. */ + /** + * Abstraction value for 'and' assign operator &=. + */ public static final String AND_ASSIGN = "__AND___ASSIGN__"; - /** Abstraction value for xor assign operator ^=. */ + /** + * Abstraction value for xor assign operator ^=. + */ public static final String XOR_ASSIGN = "__XOR___ASSIGN__"; - /** Abstraction value for 'or' assign operator |=. */ + /** + * Abstraction value for 'or' assign operator |=. + */ public static final String OR_ASSIGN = "__OR___ASSIGN__"; - /** Abstraction value for whitespace . */ + /** + * Abstraction value for whitespace . + */ public static final String WHITESPACE = "_"; - /** Abstraction value for backslash \. */ + /** + * Abstraction value for backslash \. + */ public static final String BSLASH = "__B_SLASH__"; // The preprocessor has six special operators that require additional abstraction. // These operators are documented under https://gcc.gnu.org/onlinedocs/cpp/Conditional-Syntax.html - /** Abstraction value for has_attribute operator __has_attribute(ATTRIBUTE). + /** + * Abstraction value for has_attribute operator __has_attribute(ATTRIBUTE). * One of the six special operators that require abstraction. - * */ + */ public static final String HAS_ATTRIBUTE = "HAS_ATTRIBUTE_"; - /** Abstraction value for has_cpp_attribute operator __has_cpp_attribute(ATTRIBUTE). - * One of the six special preprocessor operators that require abstraction. */ + /** + * Abstraction value for has_cpp_attribute operator __has_cpp_attribute(ATTRIBUTE). + * One of the six special preprocessor operators that require abstraction. + */ public static final String HAS_CPP_ATTRIBUTE = "HAS_CPP_ATTRIBUTE_"; - /** Abstraction value for has_c_attribute operator __has_c_attribute(ATTRIBUTE). - * One of the six special preprocessor operators that require abstraction. */ + /** + * Abstraction value for has_c_attribute operator __has_c_attribute(ATTRIBUTE). + * One of the six special preprocessor operators that require abstraction. + */ public static final String HAS_C_ATTRIBUTE = "HAS_C_ATTRIBUTE_"; - /** Abstraction value for has_builtin operator __has_builtin(BUILTIN). - * One of the six special preprocessor operators that require abstraction. */ + /** + * Abstraction value for has_builtin operator __has_builtin(BUILTIN). + * One of the six special preprocessor operators that require abstraction. + */ public static final String HAS_BUILTIN = "HAS_BUILTIN_"; - /** Abstraction value for has_include operator __has_include(INCLUDE). - * One of the six special preprocessor operators that require abstraction. */ + /** + * Abstraction value for has_include operator __has_include(INCLUDE). + * One of the six special preprocessor operators that require abstraction. + */ public static final String HAS_INCLUDE = "HAS_INCLUDE_"; - /** Abstraction value for defined operator defined. - * One of the six special preprocessor operators that require abstraction. */ + /** + * Abstraction value for defined operator defined. + * One of the six special preprocessor operators that require abstraction. + */ public static final String DEFINED = "DEFINED_"; private record Replacement(Pattern pattern, String replacement) { @@ -137,99 +242,100 @@ private record Replacement(Pattern pattern, String replacement) { private Replacement { } - /** - * 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. + * + * @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) - ); - } + /** + * 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); - } + /** + * 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 REPLACEMENTS = 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(":", COLON), - Replacement.literal( "&&", L_AND), - Replacement.literal( "||", L_OR), - Replacement.literal( ".", DOT), - Replacement.literal( "\"", QUOTE), - Replacement.literal( "'", SQUOTE), - Replacement.literal( "(", BRACKET_L), - Replacement.literal( ")", BRACKET_R), - Replacement.literal( "__has_attribute", HAS_ATTRIBUTE), - Replacement.literal( "__has_cpp_attribute", HAS_CPP_ATTRIBUTE), - Replacement.literal( "__has_c_attribute", HAS_C_ATTRIBUTE), - Replacement.literal( "__has_builtin", HAS_BUILTIN), - Replacement.literal( "__has_include", HAS_INCLUDE), - Replacement.literal( "defined", DEFINED), - Replacement.literal( "=", ASSIGN), - Replacement.literal( "*=", STAR_ASSIGN), - Replacement.literal( "/=", DIV_ASSIGN), - Replacement.literal( "%=", MOD_ASSIGN), - Replacement.literal( "+=", PLUS_ASSIGN), - Replacement.literal( "-=", MINUS_ASSIGN), - Replacement.literal( "<<=", LEFT_SHIFT_ASSIGN), - Replacement.literal( ">>=", RIGHT_SHIFT_ASSIGN), - Replacement.literal( "&=", AND_ASSIGN), - Replacement.literal( "^=", XOR_ASSIGN), - Replacement.literal( "|=", OR_ASSIGN), - Replacement.literal( "\\", BSLASH), - new Replacement( Pattern.compile("\\s+"), WHITESPACE), - Replacement.onlyFullWord("&", AND), // && has to be left untouched - Replacement.onlyFullWord("|", OR) // || has to be left untouched + // 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(":", COLON), + Replacement.literal("&&", L_AND), + Replacement.literal("||", L_OR), + Replacement.literal(".", DOT), + Replacement.literal("\"", QUOTE), + Replacement.literal("'", SQUOTE), + Replacement.literal("(", BRACKET_L), + Replacement.literal(")", BRACKET_R), + Replacement.literal("__has_attribute", HAS_ATTRIBUTE), + Replacement.literal("__has_cpp_attribute", HAS_CPP_ATTRIBUTE), + Replacement.literal("__has_c_attribute", HAS_C_ATTRIBUTE), + Replacement.literal("__has_builtin", HAS_BUILTIN), + Replacement.literal("__has_include", HAS_INCLUDE), + Replacement.literal("defined", DEFINED), + Replacement.literal("=", ASSIGN), + Replacement.literal("*=", STAR_ASSIGN), + Replacement.literal("/=", DIV_ASSIGN), + Replacement.literal("%=", MOD_ASSIGN), + Replacement.literal("+=", PLUS_ASSIGN), + Replacement.literal("-=", MINUS_ASSIGN), + Replacement.literal("<<=", LEFT_SHIFT_ASSIGN), + Replacement.literal(">>=", RIGHT_SHIFT_ASSIGN), + Replacement.literal("&=", AND_ASSIGN), + Replacement.literal("^=", XOR_ASSIGN), + Replacement.literal("|=", OR_ASSIGN), + Replacement.literal("\\", BSLASH), + new Replacement(Pattern.compile("\\s+"), WHITESPACE), + Replacement.onlyFullWord("&", AND), // && has to be left untouched + Replacement.onlyFullWord("|", OR) // || has to be left untouched ); /** * Apply all possible abstraction replacements for substrings of the given formula. + * * @param formula the formula to abstract * @return a fully abstracted formula */ diff --git a/src/main/java/org/variantsync/diffdetective/feature/CPPAnnotationParser.java b/src/main/java/org/variantsync/diffdetective/feature/CPPAnnotationParser.java deleted file mode 100644 index d413bc488..000000000 --- a/src/main/java/org/variantsync/diffdetective/feature/CPPAnnotationParser.java +++ /dev/null @@ -1,68 +0,0 @@ -package org.variantsync.diffdetective.feature; - -import org.prop4j.Literal; -import org.prop4j.Node; -import org.variantsync.diffdetective.error.UnparseableFormulaException; - -/** - * A parser of C-preprocessor annotations. - * @author Paul Bittner - */ -public class CPPAnnotationParser { - /** - * Default CPPAnnotationParser. Created by invoking {@link #CPPAnnotationParser()}. - */ - public static final CPPAnnotationParser Default = new CPPAnnotationParser(); - - private final PropositionalFormulaParser formulaParser; - private final CPPDiffLineFormulaExtractor extractor; - - /** - * Invokes {@link #CPPAnnotationParser(PropositionalFormulaParser, CPPDiffLineFormulaExtractor)} with - * the {@link PropositionalFormulaParser#Default default formula parser} and a new {@link CPPDiffLineFormulaExtractor}. - */ - public CPPAnnotationParser() { - this(PropositionalFormulaParser.Default, new CPPDiffLineFormulaExtractor()); - } - - /** - * Creates a new preprocessor annotation parser. - * @param formulaParser Parser that is used to parse propositional formulas in conditional annotations (e.g., the formula f in #if f). - * @param extractor An extractor that extracts the formula part of a preprocessor annotation that is then given to the formulaParser. - */ - public CPPAnnotationParser(final PropositionalFormulaParser formulaParser, CPPDiffLineFormulaExtractor extractor) { - this.formulaParser = formulaParser; - this.extractor = extractor; - } - - /** - * 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's condition as name. - * @throws UnparseableFormulaException when {@link CPPDiffLineFormulaExtractor#extractFormula(String)} throws. - */ - public Node parseDiffLine(String line) throws UnparseableFormulaException { - 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(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 deleted file mode 100644 index 6b853d4e4..000000000 --- a/src/main/java/org/variantsync/diffdetective/feature/CPPDiffLineFormulaExtractor.java +++ /dev/null @@ -1,125 +0,0 @@ -package org.variantsync.diffdetective.feature; - -import org.antlr.v4.runtime.*; -import org.antlr.v4.runtime.atn.ATNConfigSet; -import org.antlr.v4.runtime.dfa.DFA; -import org.antlr.v4.runtime.tree.ParseTree; -import org.tinylog.Logger; -import org.variantsync.diffdetective.error.UnparseableFormulaException; -import org.variantsync.diffdetective.error.UncheckedUnParseableFormulaException; -import org.variantsync.diffdetective.feature.antlr.CExpressionLexer; -import org.variantsync.diffdetective.feature.antlr.CExpressionParser; - -import java.util.BitSet; -import java.util.function.Supplier; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -/** - * Extracts the expression from a C preprocessor statement. - * For example, given the annotation "#if defined(A) || B()", the extractor would extract - * "A || B". The extractor detects if, ifdef, ifndef and elif annotations. - * (Other annotations do not have expressions.) - * The given pre-processor statement might also a line in a diff (i.e., preceeded by a - or +). - * @author Paul Bittner, Sören Viegener, Benjamin Moosherr - */ -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); - - /** - * Resolves any macros in the given formula that are relevant for feature annotations. - * For example, in {@link org.variantsync.diffdetective.datasets.predefined.MarlinCPPDiffLineFormulaExtractor Marlin}, - * feature annotations are given by the custom ENABLED and DISABLED macros, - * which have to be unwrapped. - * @param formula The formula whose feature macros to resolve. - * @return The parseable formula as string. The default implementation returns the input string. - */ - protected String resolveFeatureMacroFunctions(String formula) { - return formula; - } - - /** - * Extracts the feature formula as a string from a macro line (possibly within a diff). - * @param line The line of which to get the feature mapping - * @return The feature mapping as a String of the given line - */ - public String extractFormula(final String line) throws UnparseableFormulaException { - final Matcher matcher = CPP_ANNOTATION_REGEX_PATTERN.matcher(line); - final Supplier couldNotExtractFormula = () -> - new UnparseableFormulaException("Could not extract formula from line \""+ line + "\"."); - - // Retrieve the formula from the macro line - String fm; - if (matcher.find()) { - if (matcher.group(3) != null) { - fm = matcher.group(3); - } else { - fm = matcher.group(4); - } - } else { - throw couldNotExtractFormula.get(); - } - - // abstract complex formulas (e.g., if they contain arithmetics or macro calls) - try { - fm = abstractFormula(fm); - } catch (UncheckedUnParseableFormulaException e) { - throw e.inner(); - } catch (Exception e) { - Logger.warn(e); - throw new UnparseableFormulaException(e); - } - - if (fm.isEmpty()) { - throw couldNotExtractFormula.get(); - } - - // negate for ifndef - if ("ifndef".equals(matcher.group(1))) { - fm = "!(" + fm + ")"; - } - - return fm; - } - - /** - * Abstract the given formula. - *

- * First, the visitor uses ANTLR to parse the formula into a parse tree gives the tree to a {@link ControllingCExpressionVisitor}. - * The visitor traverses the tree starting from the root, searching for subtrees that must be abstracted. - * If such a subtree is found, the visitor calls an {@link AbstractingCExpressionVisitor} to abstract the part of - * the formula in the subtree. - *

- * @param formula that is to be abstracted - * @return the abstracted formula - */ - private String abstractFormula(String formula) { - CExpressionLexer lexer = new CExpressionLexer(CharStreams.fromString(formula)); - CommonTokenStream tokens = new CommonTokenStream(lexer); - CExpressionParser parser = new CExpressionParser(tokens); - parser.addErrorListener(new ANTLRErrorListener() { - @Override - public void syntaxError(Recognizer recognizer, Object o, int i, int i1, String s, RecognitionException e) { - Logger.warn("syntax error: {} ; {}", s, e); - Logger.warn("formula: {}", formula); - throw new UncheckedUnParseableFormulaException(s, e); - } - - @Override - public void reportAmbiguity(Parser parser, DFA dfa, int i, int i1, boolean b, BitSet bitSet, ATNConfigSet atnConfigSet) { - } - - @Override - public void reportAttemptingFullContext(Parser parser, DFA dfa, int i, int i1, BitSet bitSet, ATNConfigSet atnConfigSet) { - } - - @Override - public void reportContextSensitivity(Parser parser, DFA dfa, int i, int i1, int i2, ATNConfigSet atnConfigSet) { - } - }); - ParseTree tree = parser.expression(); - return tree.accept(new ControllingCExpressionVisitor()).toString(); - } -} diff --git a/src/main/java/org/variantsync/diffdetective/feature/ControllingCExpressionVisitor.java b/src/main/java/org/variantsync/diffdetective/feature/ControllingCExpressionVisitor.java deleted file mode 100644 index 658237a8c..000000000 --- a/src/main/java/org/variantsync/diffdetective/feature/ControllingCExpressionVisitor.java +++ /dev/null @@ -1,350 +0,0 @@ -package org.variantsync.diffdetective.feature; -import org.antlr.v4.runtime.*; -import org.antlr.v4.runtime.tree.AbstractParseTreeVisitor; -import org.antlr.v4.runtime.tree.ParseTree; -import org.antlr.v4.runtime.tree.TerminalNode; -import org.variantsync.diffdetective.feature.antlr.CExpressionParser; -import org.variantsync.diffdetective.feature.antlr.CExpressionVisitor; - -import java.util.function.Function; - -/** - * Visitor that controls how formulas given as an ANTLR parse tree are abstracted. - * To this end, the visitor traverses the parse tree, searching for subtrees that should be abstracted. - * If such a subtree is found, the visitor calls an {@link AbstractingCExpressionVisitor} to abstract the entire subtree. - * Only those parts of a formula are abstracted that require abstraction, leaving ancestors in the tree unchanged. - */ -@SuppressWarnings("CheckReturnValue") -public class ControllingCExpressionVisitor extends AbstractParseTreeVisitor implements CExpressionVisitor { - private final AbstractingCExpressionVisitor abstractingVisitor = new AbstractingCExpressionVisitor(); - - public ControllingCExpressionVisitor() {} - - // conditionalExpression - // : logicalOrExpression ('?' expression ':' conditionalExpression)? - // ; - @Override public StringBuilder visitConditionalExpression(CExpressionParser.ConditionalExpressionContext ctx) { - if (ctx.expression() != null) { - // logicalOrExpression '?' expression ':' conditionalExpression - // We have to abstract the expression if it is a ternary expression - return ctx.accept(abstractingVisitor); - } else { - // logicalOrExpression - return ctx.logicalOrExpression().accept(this); - } - } - - // primaryExpression - // : macroExpression - // | Identifier - // | Constant - // | StringLiteral+ - // | '(' expression ')' - // | unaryOperator primaryExpression - // | specialOperator - // ; - @Override public StringBuilder visitPrimaryExpression(CExpressionParser.PrimaryExpressionContext ctx) { - // macroExpression - if (ctx.macroExpression() != null) { - return ctx.macroExpression().accept(abstractingVisitor); - } - // Identifier - if (ctx.Identifier() != null) { - // Terminal - return ctx.accept(abstractingVisitor); - } - // Constant - if (ctx.Constant() != null) { - // Terminal - return new StringBuilder(ctx.Constant().getText().trim()); - } - // StringLiteral+ - if (!ctx.StringLiteral().isEmpty()) { - return ctx.accept(abstractingVisitor); - } - // '(' expression ')' - if (ctx.expression() != null) { - StringBuilder sb = ctx.expression().accept(this); - sb.insert(0, "("); - sb.append(")"); - return sb; - } - // unaryOperator primaryExpression - if (ctx.unaryOperator() != null) { - StringBuilder sb = ctx.unaryOperator().accept(this); - sb.append(ctx.primaryExpression().accept(this)); - return sb; - } - // specialOperator - if (ctx.specialOperator() != null) { - return ctx.specialOperator().accept(abstractingVisitor); - } - - // Unreachable - throw new IllegalStateException("Unreachable code."); - } - - // unaryOperator - // : '&' | '*' | '+' | '-' | '~' | '!' - // ; - @Override public StringBuilder visitUnaryOperator(CExpressionParser.UnaryOperatorContext ctx) { return new StringBuilder(ctx.getText()); } - - - // namespaceExpression - // : primaryExpression (':' primaryExpression)* - // ; - @Override - public StringBuilder visitNamespaceExpression(CExpressionParser.NamespaceExpressionContext ctx) { - if (ctx.primaryExpression().size() > 1) { - // primaryExpression (('*'|'/'|'%') primaryExpression)+ - // We have to abstract the arithmetic expression if there is more than one operand - return ctx.accept(abstractingVisitor); - } else { - // primaryExpression - // There is exactly one child expression - return ctx.primaryExpression(0).accept(this); - } - } - - // multiplicativeExpression - // : primaryExpression (('*'|'/'|'%') primaryExpression)* - // ; - @Override public StringBuilder visitMultiplicativeExpression(CExpressionParser.MultiplicativeExpressionContext ctx) { - if (ctx.namespaceExpression().size() > 1) { - // primaryExpression (('*'|'/'|'%') primaryExpression)+ - // We have to abstract the arithmetic expression if there is more than one operand - return ctx.accept(abstractingVisitor); - } else { - // primaryExpression - // There is exactly one child expression - return ctx.namespaceExpression(0).accept(this); - } - } - - // additiveExpression - // : multiplicativeExpression (('+'|'-') multiplicativeExpression)* - // ; - @Override public StringBuilder visitAdditiveExpression(CExpressionParser.AdditiveExpressionContext ctx) { - if (ctx.multiplicativeExpression().size() > 1) { - // multiplicativeExpression (('+'|'-') multiplicativeExpression)+ - // We have to abstract the arithmetic expression if there is more than one operand - return ctx.accept(abstractingVisitor); - } else { - // multiplicativeExpression - // There is exactly one child expression - return ctx.multiplicativeExpression(0).accept(this); - } - } - - // shiftExpression - // : additiveExpression (('<<'|'>>') additiveExpression)* - // ; - @Override public StringBuilder visitShiftExpression(CExpressionParser.ShiftExpressionContext ctx) { - if (ctx.additiveExpression().size() > 1) { - // additiveExpression (('<<'|'>>') additiveExpression)+ - // We have to abstract the shift expression if there is more than one operand - return ctx.accept(abstractingVisitor); - } else { - // additiveExpression - // There is exactly one child expression - return ctx.additiveExpression(0).accept(this); - } - } - - // relationalExpression - // : shiftExpression (('<'|'>'|'<='|'>=') shiftExpression)* - // ; - @Override public StringBuilder visitRelationalExpression(CExpressionParser.RelationalExpressionContext ctx) { - if (ctx.shiftExpression().size() > 1) { - // shiftExpression (('<'|'>'|'<='|'>=') shiftExpression)+ - // We have to abstract the relational expression if there is more than one operand - return ctx.accept(abstractingVisitor); - } else { - // shiftExpression - // There is exactly one child expression - return ctx.shiftExpression(0).accept(this); - } - } - - // equalityExpression - // : relationalExpression (('=='| '!=') relationalExpression)* - // ; - @Override public StringBuilder visitEqualityExpression(CExpressionParser.EqualityExpressionContext ctx) { - if (ctx.relationalExpression().size() > 1) { - // relationalExpression (('=='| '!=') relationalExpression)+ - // We have to abstract the equality expression if there is more than one operand - return ctx.accept(abstractingVisitor); - } else { - // relationalExpression - // There is exactly one child expression - return ctx.relationalExpression(0).accept(this); - } - } - - // specialOperator - // : HasAttribute ('(' specialOperatorArgument ')')? - // | HasCPPAttribute ('(' specialOperatorArgument ')')? - // | HasCAttribute ('(' specialOperatorArgument ')')? - // | HasBuiltin ('(' specialOperatorArgument ')')? - // | HasInclude ('(' (PathLiteral | StringLiteral) ')')? - // | Defined ('(' specialOperatorArgument ')') - // | Defined specialOperatorArgument? - // ; - @Override public StringBuilder visitSpecialOperator(CExpressionParser.SpecialOperatorContext ctx) { - // We have to abstract the special operator - return ctx.accept(abstractingVisitor); - } - - // specialOperatorArgument - // : HasAttribute - // | HasCPPAttribute - // | HasCAttribute - // | HasBuiltin - // | HasInclude - // | Defined - // | Identifier - // ; - @Override - public StringBuilder visitSpecialOperatorArgument(CExpressionParser.SpecialOperatorArgumentContext ctx) { - return ctx.accept(abstractingVisitor); - } - - // macroExpression - // : Identifier '(' argumentExpressionList? ')' - // ; - @Override - public StringBuilder visitMacroExpression(CExpressionParser.MacroExpressionContext ctx) { - return ctx.accept(abstractingVisitor); - } - - // argumentExpressionList - // : assignmentExpression (',' assignmentExpression)* - // | assignmentExpression (assignmentExpression)* - // ; - @Override - public StringBuilder visitArgumentExpressionList(CExpressionParser.ArgumentExpressionListContext ctx) { - return ctx.accept(abstractingVisitor); - } - - // assignmentExpression - // : conditionalExpression - // | DigitSequence // for - // | PathLiteral - // | StringLiteral - // | primaryExpression assignmentOperator assignmentExpression - // ; - @Override - public StringBuilder visitAssignmentExpression(CExpressionParser.AssignmentExpressionContext ctx) { - if (ctx.conditionalExpression() != null) { - return ctx.conditionalExpression().accept(this); - } else { - return ctx.accept(abstractingVisitor); - } - } - - // assignmentOperator - // : '=' | '*=' | '/=' | '%=' | '+=' | '-=' | '<<=' | '>>=' | '&=' | '^=' | '|=' - // ; - @Override - public StringBuilder visitAssignmentOperator(CExpressionParser.AssignmentOperatorContext ctx) { - return ctx.accept(abstractingVisitor); - } - - // expression - // : assignmentExpression (',' assignmentExpression)* - // ; - @Override - public StringBuilder visitExpression(CExpressionParser.ExpressionContext ctx) { - if (ctx.assignmentExpression().size() > 1) { - // assignmentExpression (',' assignmentExpression)+ - return ctx.accept(abstractingVisitor); - } else { - // assignmentExpression - return ctx.assignmentExpression(0).accept(this); - } - } - - // andExpression - // : equalityExpression ( '&' equalityExpression)* - // ; - @Override public StringBuilder visitAndExpression(CExpressionParser.AndExpressionContext ctx) { - if (ctx.equalityExpression().size() > 1) { - // equalityExpression ( '&' equalityExpression)+ - // We have to abstract the 'and' expression if there is more than one operand - return ctx.accept(abstractingVisitor); - } else { - // equalityExpression - // There is exactly one child expression - return ctx.equalityExpression(0).accept(this); - } - } - - // exclusiveOrExpression - // : andExpression ('^' andExpression)* - // ; - @Override public StringBuilder visitExclusiveOrExpression(CExpressionParser.ExclusiveOrExpressionContext ctx) { - if (ctx.andExpression().size() > 1) { - // andExpression ('^' andExpression)+ - // We have to abstract the xor expression if there is more than one operand - return ctx.accept(abstractingVisitor); - } else { - // andExpression - // There is exactly one child expression - return ctx.andExpression(0).accept(this); - } - } - - // inclusiveOrExpression - // : exclusiveOrExpression ('|' exclusiveOrExpression)* - // ; - @Override public StringBuilder visitInclusiveOrExpression(CExpressionParser.InclusiveOrExpressionContext ctx) { - if (ctx.exclusiveOrExpression().size() > 1) { - // exclusiveOrExpression ('|' exclusiveOrExpression)+ - // We have to abstract the 'or' expression if there is more than one operand - return ctx.accept(abstractingVisitor); - } else { - // exclusiveOrExpression - // There is exactly one child expression - return ctx.exclusiveOrExpression(0).accept(this); - } - } - - // logicalAndExpression - // : logicalOperand ( '&&' logicalOperand)* - // ; - @Override public StringBuilder visitLogicalAndExpression(CExpressionParser.LogicalAndExpressionContext ctx) { - return visitLogicalExpression(ctx, childExpression -> childExpression instanceof CExpressionParser.LogicalOperandContext); - } - - // logicalOrExpression - // : logicalAndExpression ( '||' logicalAndExpression)* - // ; - @Override public StringBuilder visitLogicalOrExpression(CExpressionParser.LogicalOrExpressionContext ctx) { - return visitLogicalExpression(ctx, childExpression -> childExpression instanceof CExpressionParser.LogicalAndExpressionContext); - } - - // logicalOperand - // : inclusiveOrExpression - // ; - @Override - public StringBuilder visitLogicalOperand(CExpressionParser.LogicalOperandContext ctx) { - return ctx.inclusiveOrExpression().accept(this); - } - - private StringBuilder visitLogicalExpression(ParserRuleContext expressionContext, Function instanceCheck) { - StringBuilder sb = new StringBuilder(); - for (ParseTree subtree : expressionContext.children) { - if (instanceCheck.apply(subtree)) { - // logicalAndExpression | InclusiveOrExpression - sb.append(subtree.accept(this)); - } else if (subtree instanceof TerminalNode terminal) { - // '&&' | '||' - sb.append(terminal.getText().trim()); - } else { - // loop does not work as expected - throw new IllegalStateException(); - } - } - return sb; - } -} \ No newline at end of file diff --git a/src/main/java/org/variantsync/diffdetective/feature/DiffLineFormulaExtractor.java b/src/main/java/org/variantsync/diffdetective/feature/DiffLineFormulaExtractor.java new file mode 100644 index 000000000..bfdcce3af --- /dev/null +++ b/src/main/java/org/variantsync/diffdetective/feature/DiffLineFormulaExtractor.java @@ -0,0 +1,38 @@ +package org.variantsync.diffdetective.feature; + +import org.variantsync.diffdetective.error.UnparseableFormulaException; + +/** + * Interface for extracting a formula from a line containing an annotation. + * The line might be preceded by a '-', '+', or ' '. + * For example, given the line "+#if defined(A) || B()", the extractor should extract "defined(A) || B". + * + *

+ * Further alterations of the extracted formula are allowed. For instance, the extracted formula might be abstracted + * (e.g., by simplifying the call to "defined(A)" leaving only the argument "A", or substituting it with "DEFINED_A"). + *

+ * + * @author Paul Bittner, Sören Viegener, Benjamin Moosherr, Alexander Schultheiß + */ +public interface DiffLineFormulaExtractor { + /** + * Extracts the feature formula as a string from a line (possibly within a diff). + * + * @param line The line of which to get the feature mapping + * @return The feature mapping as a String of the given line + */ + String extractFormula(final String line) throws UnparseableFormulaException; + + /** + * Resolves any macros in the given formula that are relevant for feature annotations. + * For example, in {@link org.variantsync.diffdetective.datasets.predefined.MarlinCPPDiffLineFormulaExtractor Marlin}, + * feature annotations are given by the custom ENABLED and DISABLED macros, + * which have to be unwrapped. + * + * @param formula The formula whose feature macros to resolve. + * @return The parseable formula as string. The default implementation returns the input string. + */ + default String resolveFeatureMacroFunctions(String formula) { + return formula; + } +} diff --git a/src/main/java/org/variantsync/diffdetective/feature/ParseErrorListener.java b/src/main/java/org/variantsync/diffdetective/feature/ParseErrorListener.java new file mode 100644 index 000000000..2a50916fc --- /dev/null +++ b/src/main/java/org/variantsync/diffdetective/feature/ParseErrorListener.java @@ -0,0 +1,51 @@ +package org.variantsync.diffdetective.feature; + +import org.antlr.v4.runtime.ANTLRErrorListener; +import org.antlr.v4.runtime.Parser; +import org.antlr.v4.runtime.RecognitionException; +import org.antlr.v4.runtime.Recognizer; +import org.antlr.v4.runtime.atn.ATNConfigSet; +import org.antlr.v4.runtime.dfa.DFA; +import org.tinylog.Logger; +import org.variantsync.diffdetective.error.UncheckedUnParseableFormulaException; + +import java.util.BitSet; + +/** + * A ParseErrorListener listens to syntactical errors discovered by an ANTLR parser while parsing a text. Encountered + * errors are logged as warnings so that they can later be analyzed. + *

+ * Logged warning might indicate that the ANTLR grammar used for parsing is imprecise or incomplete. However, it might + * also simply be the case that the input text is indeed syntactically invalid. + *

+ * @author Alexander Schultheiß + */ +public class ParseErrorListener implements ANTLRErrorListener { + private final String formula; + + public ParseErrorListener(String formula) { + this.formula = formula; + } + + @Override + public void syntaxError(Recognizer recognizer, Object o, int i, int i1, String s, RecognitionException e) { + Logger.warn("syntax error: {} ; {}", s, e); + Logger.warn("formula: {}", formula); + throw new UncheckedUnParseableFormulaException(s, e); + } + + @Override + public void reportAmbiguity(Parser parser, DFA dfa, int i, int i1, boolean b, BitSet bitSet, ATNConfigSet atnConfigSet) { + // Do nothing + } + + @Override + public void reportAttemptingFullContext(Parser parser, DFA dfa, int i, int i1, BitSet bitSet, ATNConfigSet atnConfigSet) { + // Do nothing + } + + @Override + public void reportContextSensitivity(Parser parser, DFA dfa, int i, int i1, int i2, ATNConfigSet atnConfigSet) { + // Do nothing + } +} diff --git a/src/main/java/org/variantsync/diffdetective/feature/PreprocessorAnnotationParser.java b/src/main/java/org/variantsync/diffdetective/feature/PreprocessorAnnotationParser.java new file mode 100644 index 000000000..5828a61b6 --- /dev/null +++ b/src/main/java/org/variantsync/diffdetective/feature/PreprocessorAnnotationParser.java @@ -0,0 +1,119 @@ +package org.variantsync.diffdetective.feature; + +import org.prop4j.Node; +import org.variantsync.diffdetective.error.UnparseableFormulaException; +import org.variantsync.diffdetective.feature.cpp.CPPDiffLineFormulaExtractor; +import org.variantsync.diffdetective.feature.jpp.JPPDiffLineFormulaExtractor; + +import java.util.regex.Pattern; + +/** + * A parser of preprocessor-like annotations. + * + * @author Paul Bittner, Alexander Schultheiß + */ +public class PreprocessorAnnotationParser implements AnnotationParser { + /** + * Matches the beginning or end of CPP conditional macros. + * It doesn't match the whole macro name, for example for {@code #ifdef} only {@code "#if"} is + * matched and only {@code "if"} is captured. + *

+ * Note that this pattern doesn't handle comments between {@code #} and the macro name. + */ + protected final static Pattern CPP_PATTERN = + Pattern.compile("^[+-]?\\s*#\\s*(if|elif|else|endif)"); + + /** + * Matches the beginning or end of JPP conditional macros. + * It doesn't match the whole macro name, for example for {@code //#if defined(x)} only {@code "//#if"} is + * matched and only {@code "if"} is captured. + *

+ */ + protected final static Pattern JPP_PATTERN = + Pattern.compile("^[+-]?\\s*//\\s*#\\s*(if|elif|else|endif)"); + + /** + * Default parser for C preprocessor annotations. + * Created by invoking {@link #PreprocessorAnnotationParser(Pattern, PropositionalFormulaParser, DiffLineFormulaExtractor)}. + */ + public static final PreprocessorAnnotationParser CPPAnnotationParser = + new PreprocessorAnnotationParser(CPP_PATTERN, PropositionalFormulaParser.Default, new CPPDiffLineFormulaExtractor()); + + /** + * Default parser for JavaPP (Java PreProcessor) annotations. + * Created by invoking {@link #PreprocessorAnnotationParser(Pattern, PropositionalFormulaParser, DiffLineFormulaExtractor)}. + */ + public static final PreprocessorAnnotationParser JPPAnnotationParser = + new PreprocessorAnnotationParser(JPP_PATTERN, PropositionalFormulaParser.Default, new JPPDiffLineFormulaExtractor()); + + // Pattern that is used to identify the AnnotationType of a given annotation. + private final Pattern annotationPattern; + private final PropositionalFormulaParser formulaParser; + private final DiffLineFormulaExtractor extractor; + + /** + * Invokes {@link #PreprocessorAnnotationParser(Pattern, PropositionalFormulaParser, DiffLineFormulaExtractor)} with + * the {@link PropositionalFormulaParser#Default default formula parser} and a new {@link DiffLineFormulaExtractor}. + * + * @param annotationPattern Pattern that is used to identify the AnnotationType of a given annotation; {@link #CPP_PATTERN} provides an example + */ + public PreprocessorAnnotationParser(final Pattern annotationPattern, final DiffLineFormulaExtractor formulaExtractor) { + this(annotationPattern, PropositionalFormulaParser.Default, formulaExtractor); + } + + /** + * Creates a new preprocessor annotation parser. + * + * @param annotationPattern Pattern that is used to identify the AnnotationType of a given annotation; {@link #CPP_PATTERN} provides an example + * @param formulaParser Parser that is used to parse propositional formulas in conditional annotations (e.g., the formula f in #if f). + * @param formulaExtractor An extractor that extracts the formula part of a preprocessor annotation that is then given to the formulaParser. + */ + public PreprocessorAnnotationParser(final Pattern annotationPattern, final PropositionalFormulaParser formulaParser, DiffLineFormulaExtractor formulaExtractor) { + this.annotationPattern = annotationPattern; + this.formulaParser = formulaParser; + this.extractor = formulaExtractor; + } + + /** + * Creates a new preprocessor annotation parser for C preprocessor annotations. + * + * @param formulaParser Parser that is used to parse propositional formulas in conditional annotations (e.g., the formula f in #if f). + * @param formulaExtractor An extractor that extracts the formula part of a preprocessor annotation that is then given to the formulaParser. + */ + public static PreprocessorAnnotationParser CreateCppAnnotationParser(final PropositionalFormulaParser formulaParser, DiffLineFormulaExtractor formulaExtractor) { + return new PreprocessorAnnotationParser(CPP_PATTERN, formulaParser, formulaExtractor); + } + + /** + * Creates a new preprocessor annotation parser for JavaPP (Java PreProcessor) annotations. + * + * @param formulaParser Parser that is used to parse propositional formulas in conditional annotations (e.g., the formula f in #if f). + * @param formulaExtractor An extractor that extracts the formula part of a preprocessor annotation that is then given to the formulaParser. + */ + public static PreprocessorAnnotationParser CreateJppAnnotationParser(final PropositionalFormulaParser formulaParser, DiffLineFormulaExtractor formulaExtractor) { + return new PreprocessorAnnotationParser(JPP_PATTERN, formulaParser, formulaExtractor); + } + + /** + * 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's condition as name. + * @throws UnparseableFormulaException when {@link DiffLineFormulaExtractor#extractFormula(String)} throws. + */ + public Node parseAnnotation(String line) throws UnparseableFormulaException { + return this.formulaParser.parse(extractor.extractFormula(line)); + } + + @Override + public AnnotationType determineAnnotationType(String text) { + var matcher = annotationPattern.matcher(text); + int nameId = 1; + if (matcher.find()) { + return AnnotationType.fromName(matcher.group(nameId)); + } else { + return AnnotationType.None; + } + } +} diff --git a/src/main/java/org/variantsync/diffdetective/feature/PropositionalFormulaParser.java b/src/main/java/org/variantsync/diffdetective/feature/PropositionalFormulaParser.java index f24caad75..b470af7d3 100644 --- a/src/main/java/org/variantsync/diffdetective/feature/PropositionalFormulaParser.java +++ b/src/main/java/org/variantsync/diffdetective/feature/PropositionalFormulaParser.java @@ -1,5 +1,6 @@ package org.variantsync.diffdetective.feature; +import org.prop4j.Literal; import org.prop4j.Node; import org.prop4j.NodeReader; import org.variantsync.diffdetective.util.fide.FixTrueFalse; @@ -36,6 +37,11 @@ public interface PropositionalFormulaParser { node = FixTrueFalse.EliminateTrueAndFalseInplace(node).get(); } + if (node == null) { +// Logger.warn("Could not parse expression '{}' to feature mapping. Using it as literal.", fmString); + node = new Literal(text); + } + return node; }; } diff --git a/src/main/java/org/variantsync/diffdetective/feature/cpp/AbstractingCExpressionVisitor.java b/src/main/java/org/variantsync/diffdetective/feature/cpp/AbstractingCExpressionVisitor.java new file mode 100644 index 000000000..c317e4e20 --- /dev/null +++ b/src/main/java/org/variantsync/diffdetective/feature/cpp/AbstractingCExpressionVisitor.java @@ -0,0 +1,342 @@ +package org.variantsync.diffdetective.feature.cpp; + +import org.antlr.v4.runtime.ParserRuleContext; +import org.antlr.v4.runtime.tree.AbstractParseTreeVisitor; +import org.antlr.v4.runtime.tree.ParseTree; +import org.antlr.v4.runtime.tree.TerminalNode; +import org.variantsync.diffdetective.feature.BooleanAbstraction; +import org.variantsync.diffdetective.feature.antlr.CExpressionParser; +import org.variantsync.diffdetective.feature.antlr.CExpressionVisitor; + +import java.util.function.Function; + +/** + * Visitor that abstracts all symbols of a formula, given as ANTLR parse tree, that might interfere with further formula analysis. + * This visitor traverses the given tree and substitutes all formula substrings with replacements by calling {@link BooleanAbstraction}. + * + *

+ * Not all formulas or parts of a formula might require abstraction (e.g., 'A && B'). Therefore, this visitor should not be used directly. + * Instead, you may use a {@link ControllingCExpressionVisitor} which internally uses an {@link AbstractingCExpressionVisitor} + * to control how formulas are abstracted, and only abstracts those parts of a formula that require it. + *

+ */ +@SuppressWarnings("CheckReturnValue") +public class AbstractingCExpressionVisitor extends AbstractParseTreeVisitor implements CExpressionVisitor { + + public AbstractingCExpressionVisitor() { + } + + // conditionalExpression + // : logicalOrExpression ('?' expression ':' conditionalExpression)? + // ; + @Override + public StringBuilder visitConditionalExpression(CExpressionParser.ConditionalExpressionContext ctx) { + return visitExpression(ctx, + childContext -> childContext instanceof CExpressionParser.LogicalOrExpressionContext + || childContext instanceof CExpressionParser.ExpressionContext + || childContext instanceof CExpressionParser.ConditionalExpressionContext); + } + + // primaryExpression + // : macroExpression + // | Identifier + // | Constant + // | StringLiteral+ + // | '(' expression ')' + // | unaryOperator primaryExpression + // | specialOperator + // ; + @Override + public StringBuilder visitPrimaryExpression(CExpressionParser.PrimaryExpressionContext ctx) { + // macroExpression + if (ctx.macroExpression() != null) { + return ctx.macroExpression().accept(this); + } + // Identifier + if (ctx.Identifier() != null) { + // Terminal + return new StringBuilder(BooleanAbstraction.abstractAll(ctx.Identifier().getText().trim())); + } + // Constant + if (ctx.Constant() != null) { + // Terminal + return new StringBuilder(BooleanAbstraction.abstractAll(ctx.Constant().getText().trim())); + } + // StringLiteral+ + if (!ctx.StringLiteral().isEmpty()) { + // Terminal + StringBuilder sb = new StringBuilder(); + ctx.StringLiteral().stream().map(ParseTree::getText).map(String::trim).map(BooleanAbstraction::abstractAll).forEach(sb::append); + return sb; + } + // '(' expression ')' + if (ctx.expression() != null) { + StringBuilder sb = ctx.expression().accept(this); + sb.insert(0, BooleanAbstraction.BRACKET_L); + sb.append(BooleanAbstraction.BRACKET_R); + return sb; + } + // unaryOperator primaryExpression + if (ctx.unaryOperator() != null) { + StringBuilder sb = ctx.unaryOperator().accept(this); + sb.append(ctx.primaryExpression().accept(this)); + return sb; + } + // specialOperator + if (ctx.specialOperator() != null) { + return ctx.specialOperator().accept(this); + } + + // Unreachable + throw new IllegalStateException("Unreachable code."); + } + + // unaryOperator + // : '&' | '*' | '+' | '-' | '~' | '!' + // ; + @Override + public StringBuilder visitUnaryOperator(CExpressionParser.UnaryOperatorContext ctx) { + if (ctx.And() != null) { + return new StringBuilder(BooleanAbstraction.U_AND); + } + if (ctx.Star() != null) { + return new StringBuilder(BooleanAbstraction.U_STAR); + } + if (ctx.Plus() != null) { + return new StringBuilder(BooleanAbstraction.U_PLUS); + } + if (ctx.Minus() != null) { + return new StringBuilder(BooleanAbstraction.U_MINUS); + } + if (ctx.Tilde() != null) { + return new StringBuilder(BooleanAbstraction.U_TILDE); + } + if (ctx.Not() != null) { + return new StringBuilder(BooleanAbstraction.U_NOT); + } + throw new IllegalStateException(); + } + + // namespaceExpression + // : primaryExpression (':' primaryExpression)* + // ; + @Override + public StringBuilder visitNamespaceExpression(CExpressionParser.NamespaceExpressionContext ctx) { + return visitExpression(ctx, childContext -> childContext instanceof CExpressionParser.PrimaryExpressionContext); + } + + // multiplicativeExpression + // : namespaceExpression (('*'|'/'|'%') namespaceExpression)* + // ; + @Override + public StringBuilder visitMultiplicativeExpression(CExpressionParser.MultiplicativeExpressionContext ctx) { + return visitExpression(ctx, childContext -> childContext instanceof CExpressionParser.NamespaceExpressionContext); + } + + // additiveExpression + // : multiplicativeExpression (('+'|'-') multiplicativeExpression)* + // ; + @Override + public StringBuilder visitAdditiveExpression(CExpressionParser.AdditiveExpressionContext ctx) { + return visitExpression(ctx, childContext -> childContext instanceof CExpressionParser.MultiplicativeExpressionContext); + } + + // shiftExpression + // : additiveExpression (('<<'|'>>') additiveExpression)* + // ; + @Override + public StringBuilder visitShiftExpression(CExpressionParser.ShiftExpressionContext ctx) { + return visitExpression(ctx, childContext -> childContext instanceof CExpressionParser.AdditiveExpressionContext); + } + + // relationalExpression + // : shiftExpression (('<'|'>'|'<='|'>=') shiftExpression)* + // ; + @Override + public StringBuilder visitRelationalExpression(CExpressionParser.RelationalExpressionContext ctx) { + return visitExpression(ctx, childContext -> childContext instanceof CExpressionParser.ShiftExpressionContext); + } + + // equalityExpression + // : relationalExpression (('=='| '!=') relationalExpression)* + // ; + @Override + public StringBuilder visitEqualityExpression(CExpressionParser.EqualityExpressionContext ctx) { + return visitExpression(ctx, childContext -> childContext instanceof CExpressionParser.RelationalExpressionContext); + } + + // andExpression + // : equalityExpression ( '&' equalityExpression)* + // ; + @Override + public StringBuilder visitAndExpression(CExpressionParser.AndExpressionContext ctx) { + return visitExpression(ctx, childContext -> childContext instanceof CExpressionParser.EqualityExpressionContext); + } + + // exclusiveOrExpression + // : andExpression ('^' andExpression)* + // ; + @Override + public StringBuilder visitExclusiveOrExpression(CExpressionParser.ExclusiveOrExpressionContext ctx) { + return visitExpression(ctx, childContext -> childContext instanceof CExpressionParser.AndExpressionContext); + } + + // inclusiveOrExpression + // : exclusiveOrExpression ('|' exclusiveOrExpression)* + // ; + @Override + public StringBuilder visitInclusiveOrExpression(CExpressionParser.InclusiveOrExpressionContext ctx) { + return visitExpression(ctx, childContext -> childContext instanceof CExpressionParser.ExclusiveOrExpressionContext); + } + + // specialOperator + // : HasAttribute ('(' specialOperatorArgument ')')? + // | HasCPPAttribute ('(' specialOperatorArgument ')')? + // | HasCAttribute ('(' specialOperatorArgument ')')? + // | HasBuiltin ('(' specialOperatorArgument ')')? + // | HasInclude ('(' specialOperatorArgument ')')? + // | Defined ('(' specialOperatorArgument ')') + // | Defined specialOperatorArgument? + // ; + @Override + public StringBuilder visitSpecialOperator(CExpressionParser.SpecialOperatorContext ctx) { + return visitExpression(ctx, childContext -> childContext instanceof CExpressionParser.SpecialOperatorArgumentContext); + } + + // specialOperatorArgument + // : HasAttribute + // | HasCPPAttribute + // | HasCAttribute + // | HasBuiltin + // | HasInclude + // | Defined + // | Identifier + // | PathLiteral + // | StringLiteral + // ; + @Override + public StringBuilder visitSpecialOperatorArgument(CExpressionParser.SpecialOperatorArgumentContext ctx) { + return new StringBuilder(BooleanAbstraction.abstractAll(ctx.getText().trim())); + } + + // logicalAndExpression + // : logicalOperand ( '&&' logicalOperand)* + // ; + @Override + public StringBuilder visitLogicalAndExpression(CExpressionParser.LogicalAndExpressionContext ctx) { + return visitExpression(ctx, childExpression -> childExpression instanceof CExpressionParser.LogicalOperandContext); + } + + // logicalOrExpression + // : logicalAndExpression ( '||' logicalAndExpression)* + // ; + @Override + public StringBuilder visitLogicalOrExpression(CExpressionParser.LogicalOrExpressionContext ctx) { + return visitExpression(ctx, childExpression -> childExpression instanceof CExpressionParser.LogicalAndExpressionContext); + } + + // logicalOperand + // : inclusiveOrExpression + // ; + @Override + public StringBuilder visitLogicalOperand(CExpressionParser.LogicalOperandContext ctx) { + return ctx.inclusiveOrExpression().accept(this); + } + + // macroExpression + // : Identifier '(' argumentExpressionList? ')' + // ; + @Override + public StringBuilder visitMacroExpression(CExpressionParser.MacroExpressionContext ctx) { + StringBuilder sb = new StringBuilder(); + sb.append(ctx.Identifier().getText().trim().toUpperCase()).append("_"); + if (ctx.argumentExpressionList() != null) { + sb.append(ctx.argumentExpressionList().accept(this)); + } + return sb; + } + + // argumentExpressionList + // : assignmentExpression (',' assignmentExpression)* + // | assignmentExpression (assignmentExpression)* + // ; + @Override + public StringBuilder visitArgumentExpressionList(CExpressionParser.ArgumentExpressionListContext ctx) { + StringBuilder sb = new StringBuilder(); + sb.append(BooleanAbstraction.BRACKET_L); + for (int i = 0; i < ctx.assignmentExpression().size(); i++) { + sb.append(ctx.assignmentExpression(i).accept(this)); + if (i < ctx.assignmentExpression().size() - 1) { + // For each ',' separating arguments + sb.append("__"); + } + } + sb.append(BooleanAbstraction.BRACKET_R); + return sb; + } + + // assignmentExpression + // : conditionalExpression + // | DigitSequence // for + // | PathLiteral + // | StringLiteral + // | primaryExpression assignmentOperator assignmentExpression + // ; + @Override + public StringBuilder visitAssignmentExpression(CExpressionParser.AssignmentExpressionContext ctx) { + if (ctx.conditionalExpression() != null) { + // conditionalExpression + return ctx.conditionalExpression().accept(this); + } else if (ctx.primaryExpression() != null) { + // primaryExpression assignmentOperator assignmentExpression + StringBuilder sb = new StringBuilder(); + sb.append(ctx.primaryExpression().accept(this)); + sb.append(ctx.assignmentOperator().accept(this)); + sb.append(ctx.assignmentExpression().accept(this)); + return sb; + } else { + // all other cases require direct abstraction + return new StringBuilder(BooleanAbstraction.abstractAll(ctx.getText().trim())); + } + } + + // assignmentOperator + // : '=' | '*=' | '/=' | '%=' | '+=' | '-=' | '<<=' | '>>=' | '&=' | '^=' | '|=' + // ; + @Override + public StringBuilder visitAssignmentOperator(CExpressionParser.AssignmentOperatorContext ctx) { + return new StringBuilder(BooleanAbstraction.abstractToken(ctx.getText().trim())); + } + + // expression + // : assignmentExpression (',' assignmentExpression)* + // ; + @Override + public StringBuilder visitExpression(CExpressionParser.ExpressionContext ctx) { + return visitExpression(ctx, childContext -> childContext instanceof CExpressionParser.AssignmentExpressionContext); + } + + /** + * Abstract all child nodes in the parse tree. + * + * @param expressionContext The root of the subtree to abstract + * @param instanceCheck A check for expected child node types + * @return The abstracted formula of the subtree + */ + private StringBuilder visitExpression(ParserRuleContext expressionContext, Function instanceCheck) { + StringBuilder sb = new StringBuilder(); + for (ParseTree subtree : expressionContext.children) { + if (instanceCheck.apply(subtree)) { + // Some operand (i.e., a subtree) that we have to visit + sb.append(subtree.accept(this)); + } else if (subtree instanceof TerminalNode terminal) { + // Some operator (i.e., a leaf node) that requires direct abstraction + sb.append(BooleanAbstraction.abstractToken(terminal.getText().trim())); + } else { + // sanity check: loop does not work as expected + throw new IllegalStateException(); + } + } + return sb; + } +} \ No newline at end of file diff --git a/src/main/java/org/variantsync/diffdetective/feature/cpp/CPPDiffLineFormulaExtractor.java b/src/main/java/org/variantsync/diffdetective/feature/cpp/CPPDiffLineFormulaExtractor.java new file mode 100644 index 000000000..9d77c5c23 --- /dev/null +++ b/src/main/java/org/variantsync/diffdetective/feature/cpp/CPPDiffLineFormulaExtractor.java @@ -0,0 +1,74 @@ +package org.variantsync.diffdetective.feature.cpp; + +import org.antlr.v4.runtime.CharStreams; +import org.antlr.v4.runtime.CommonTokenStream; +import org.variantsync.diffdetective.error.UnparseableFormulaException; +import org.variantsync.diffdetective.feature.AbstractingFormulaExtractor; +import org.variantsync.diffdetective.feature.ParseErrorListener; +import org.variantsync.diffdetective.feature.antlr.CExpressionLexer; +import org.variantsync.diffdetective.feature.antlr.CExpressionParser; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Extracts the expression from a C preprocessor statement. + * For example, given the annotation "#if defined(A) || B()", the extractor would extract + * "A || B". The extractor detects if, ifdef, ifndef and elif annotations. + * (Other annotations do not have expressions.) + * The given pre-processor statement might also a line in a diff (i.e., preceeded by a - or +). + * + * @author Paul Bittner, Sören Viegener, Benjamin Moosherr, Alexander Schultheiß + */ +public class CPPDiffLineFormulaExtractor extends AbstractingFormulaExtractor { + // ^[+-]?\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_PATTERN = Pattern.compile(CPP_ANNOTATION_REGEX); + + public CPPDiffLineFormulaExtractor() { + super(CPP_ANNOTATION_PATTERN); + } + + /** + * Extracts the feature formula as a string from a macro line (possibly within a diff). + * + * @param line The line of which to get the feature mapping + * @return The feature mapping as a String of the given line + */ + @Override + public String extractFormula(final String line) throws UnparseableFormulaException { + // Delegate the formula extraction to AbstractingFormulaExtractor + String fm = super.extractFormula(line); + + // negate for ifndef + final Matcher matcher = CPP_ANNOTATION_PATTERN.matcher(line); + if (matcher.find() && "ifndef".equals(matcher.group(1))) { + fm = "!(" + fm + ")"; + } + + return fm; + } + + /** + * Abstract the given formula. + *

+ * First, the visitor uses ANTLR to parse the formula into a parse tree gives the tree to a {@link ControllingCExpressionVisitor}. + * The visitor traverses the tree starting from the root, searching for subtrees that must be abstracted. + * If such a subtree is found, the visitor calls an {@link AbstractingCExpressionVisitor} to abstract the part of + * the formula in the subtree. + *

+ * + * @param formula that is to be abstracted + * @return the abstracted formula + */ + @Override + protected String abstractFormula(String formula) { + CExpressionLexer lexer = new CExpressionLexer(CharStreams.fromString(formula)); + CommonTokenStream tokens = new CommonTokenStream(lexer); + + CExpressionParser parser = new CExpressionParser(tokens); + parser.addErrorListener(new ParseErrorListener(formula)); + + return parser.expression().accept(new ControllingCExpressionVisitor()).toString(); + } +} diff --git a/src/main/java/org/variantsync/diffdetective/feature/cpp/ControllingCExpressionVisitor.java b/src/main/java/org/variantsync/diffdetective/feature/cpp/ControllingCExpressionVisitor.java new file mode 100644 index 000000000..8bc9fed28 --- /dev/null +++ b/src/main/java/org/variantsync/diffdetective/feature/cpp/ControllingCExpressionVisitor.java @@ -0,0 +1,368 @@ +package org.variantsync.diffdetective.feature.cpp; + +import org.antlr.v4.runtime.ParserRuleContext; +import org.antlr.v4.runtime.tree.AbstractParseTreeVisitor; +import org.antlr.v4.runtime.tree.ParseTree; +import org.antlr.v4.runtime.tree.TerminalNode; +import org.variantsync.diffdetective.feature.antlr.CExpressionParser; +import org.variantsync.diffdetective.feature.antlr.CExpressionVisitor; + +import java.util.function.Function; + +/** + * Visitor that controls how formulas given as an ANTLR parse tree are abstracted. + * To this end, the visitor traverses the parse tree, searching for subtrees that should be abstracted. + * If such a subtree is found, the visitor calls an {@link AbstractingCExpressionVisitor} to abstract the entire subtree. + * Only those parts of a formula are abstracted that require abstraction, leaving ancestors in the tree unchanged. + */ +@SuppressWarnings("CheckReturnValue") +public class ControllingCExpressionVisitor extends AbstractParseTreeVisitor implements CExpressionVisitor { + private final AbstractingCExpressionVisitor abstractingVisitor = new AbstractingCExpressionVisitor(); + + public ControllingCExpressionVisitor() { + } + + // conditionalExpression + // : logicalOrExpression ('?' expression ':' conditionalExpression)? + // ; + @Override + public StringBuilder visitConditionalExpression(CExpressionParser.ConditionalExpressionContext ctx) { + if (ctx.expression() != null) { + // logicalOrExpression '?' expression ':' conditionalExpression + // We have to abstract the expression if it is a ternary expression + return ctx.accept(abstractingVisitor); + } else { + // logicalOrExpression + return ctx.logicalOrExpression().accept(this); + } + } + + // primaryExpression + // : macroExpression + // | Identifier + // | Constant + // | StringLiteral+ + // | '(' expression ')' + // | unaryOperator primaryExpression + // | specialOperator + // ; + @Override + public StringBuilder visitPrimaryExpression(CExpressionParser.PrimaryExpressionContext ctx) { + // macroExpression + if (ctx.macroExpression() != null) { + return ctx.macroExpression().accept(abstractingVisitor); + } + // Identifier + if (ctx.Identifier() != null) { + // Terminal + return ctx.accept(abstractingVisitor); + } + // Constant + if (ctx.Constant() != null) { + // Terminal + return new StringBuilder(ctx.Constant().getText().trim()); + } + // StringLiteral+ + if (!ctx.StringLiteral().isEmpty()) { + return ctx.accept(abstractingVisitor); + } + // '(' expression ')' + if (ctx.expression() != null) { + StringBuilder sb = ctx.expression().accept(this); + sb.insert(0, "("); + sb.append(")"); + return sb; + } + // unaryOperator primaryExpression + if (ctx.unaryOperator() != null) { + StringBuilder sb = ctx.unaryOperator().accept(this); + sb.append(ctx.primaryExpression().accept(this)); + return sb; + } + // specialOperator + if (ctx.specialOperator() != null) { + return ctx.specialOperator().accept(abstractingVisitor); + } + + // Unreachable + throw new IllegalStateException("Unreachable code."); + } + + // unaryOperator + // : '&' | '*' | '+' | '-' | '~' | '!' + // ; + @Override + public StringBuilder visitUnaryOperator(CExpressionParser.UnaryOperatorContext ctx) { + return new StringBuilder(ctx.getText()); + } + + + // namespaceExpression + // : primaryExpression (':' primaryExpression)* + // ; + @Override + public StringBuilder visitNamespaceExpression(CExpressionParser.NamespaceExpressionContext ctx) { + if (ctx.primaryExpression().size() > 1) { + // primaryExpression (('*'|'/'|'%') primaryExpression)+ + // We have to abstract the arithmetic expression if there is more than one operand + return ctx.accept(abstractingVisitor); + } else { + // primaryExpression + // There is exactly one child expression + return ctx.primaryExpression(0).accept(this); + } + } + + // multiplicativeExpression + // : primaryExpression (('*'|'/'|'%') primaryExpression)* + // ; + @Override + public StringBuilder visitMultiplicativeExpression(CExpressionParser.MultiplicativeExpressionContext ctx) { + if (ctx.namespaceExpression().size() > 1) { + // primaryExpression (('*'|'/'|'%') primaryExpression)+ + // We have to abstract the arithmetic expression if there is more than one operand + return ctx.accept(abstractingVisitor); + } else { + // primaryExpression + // There is exactly one child expression + return ctx.namespaceExpression(0).accept(this); + } + } + + // additiveExpression + // : multiplicativeExpression (('+'|'-') multiplicativeExpression)* + // ; + @Override + public StringBuilder visitAdditiveExpression(CExpressionParser.AdditiveExpressionContext ctx) { + if (ctx.multiplicativeExpression().size() > 1) { + // multiplicativeExpression (('+'|'-') multiplicativeExpression)+ + // We have to abstract the arithmetic expression if there is more than one operand + return ctx.accept(abstractingVisitor); + } else { + // multiplicativeExpression + // There is exactly one child expression + return ctx.multiplicativeExpression(0).accept(this); + } + } + + // shiftExpression + // : additiveExpression (('<<'|'>>') additiveExpression)* + // ; + @Override + public StringBuilder visitShiftExpression(CExpressionParser.ShiftExpressionContext ctx) { + if (ctx.additiveExpression().size() > 1) { + // additiveExpression (('<<'|'>>') additiveExpression)+ + // We have to abstract the shift expression if there is more than one operand + return ctx.accept(abstractingVisitor); + } else { + // additiveExpression + // There is exactly one child expression + return ctx.additiveExpression(0).accept(this); + } + } + + // relationalExpression + // : shiftExpression (('<'|'>'|'<='|'>=') shiftExpression)* + // ; + @Override + public StringBuilder visitRelationalExpression(CExpressionParser.RelationalExpressionContext ctx) { + if (ctx.shiftExpression().size() > 1) { + // shiftExpression (('<'|'>'|'<='|'>=') shiftExpression)+ + // We have to abstract the relational expression if there is more than one operand + return ctx.accept(abstractingVisitor); + } else { + // shiftExpression + // There is exactly one child expression + return ctx.shiftExpression(0).accept(this); + } + } + + // equalityExpression + // : relationalExpression (('=='| '!=') relationalExpression)* + // ; + @Override + public StringBuilder visitEqualityExpression(CExpressionParser.EqualityExpressionContext ctx) { + if (ctx.relationalExpression().size() > 1) { + // relationalExpression (('=='| '!=') relationalExpression)+ + // We have to abstract the equality expression if there is more than one operand + return ctx.accept(abstractingVisitor); + } else { + // relationalExpression + // There is exactly one child expression + return ctx.relationalExpression(0).accept(this); + } + } + + // specialOperator + // : HasAttribute ('(' specialOperatorArgument ')')? + // | HasCPPAttribute ('(' specialOperatorArgument ')')? + // | HasCAttribute ('(' specialOperatorArgument ')')? + // | HasBuiltin ('(' specialOperatorArgument ')')? + // | HasInclude ('(' (PathLiteral | StringLiteral) ')')? + // | Defined ('(' specialOperatorArgument ')') + // | Defined specialOperatorArgument? + // ; + @Override + public StringBuilder visitSpecialOperator(CExpressionParser.SpecialOperatorContext ctx) { + // We have to abstract the special operator + return ctx.accept(abstractingVisitor); + } + + // specialOperatorArgument + // : HasAttribute + // | HasCPPAttribute + // | HasCAttribute + // | HasBuiltin + // | HasInclude + // | Defined + // | Identifier + // ; + @Override + public StringBuilder visitSpecialOperatorArgument(CExpressionParser.SpecialOperatorArgumentContext ctx) { + return ctx.accept(abstractingVisitor); + } + + // macroExpression + // : Identifier '(' argumentExpressionList? ')' + // ; + @Override + public StringBuilder visitMacroExpression(CExpressionParser.MacroExpressionContext ctx) { + return ctx.accept(abstractingVisitor); + } + + // argumentExpressionList + // : assignmentExpression (',' assignmentExpression)* + // | assignmentExpression (assignmentExpression)* + // ; + @Override + public StringBuilder visitArgumentExpressionList(CExpressionParser.ArgumentExpressionListContext ctx) { + return ctx.accept(abstractingVisitor); + } + + // assignmentExpression + // : conditionalExpression + // | DigitSequence // for + // | PathLiteral + // | StringLiteral + // | primaryExpression assignmentOperator assignmentExpression + // ; + @Override + public StringBuilder visitAssignmentExpression(CExpressionParser.AssignmentExpressionContext ctx) { + if (ctx.conditionalExpression() != null) { + return ctx.conditionalExpression().accept(this); + } else { + return ctx.accept(abstractingVisitor); + } + } + + // assignmentOperator + // : '=' | '*=' | '/=' | '%=' | '+=' | '-=' | '<<=' | '>>=' | '&=' | '^=' | '|=' + // ; + @Override + public StringBuilder visitAssignmentOperator(CExpressionParser.AssignmentOperatorContext ctx) { + return ctx.accept(abstractingVisitor); + } + + // expression + // : assignmentExpression (',' assignmentExpression)* + // ; + @Override + public StringBuilder visitExpression(CExpressionParser.ExpressionContext ctx) { + if (ctx.assignmentExpression().size() > 1) { + // assignmentExpression (',' assignmentExpression)+ + return ctx.accept(abstractingVisitor); + } else { + // assignmentExpression + return ctx.assignmentExpression(0).accept(this); + } + } + + // andExpression + // : equalityExpression ( '&' equalityExpression)* + // ; + @Override + public StringBuilder visitAndExpression(CExpressionParser.AndExpressionContext ctx) { + if (ctx.equalityExpression().size() > 1) { + // equalityExpression ( '&' equalityExpression)+ + // We have to abstract the 'and' expression if there is more than one operand + return ctx.accept(abstractingVisitor); + } else { + // equalityExpression + // There is exactly one child expression + return ctx.equalityExpression(0).accept(this); + } + } + + // exclusiveOrExpression + // : andExpression ('^' andExpression)* + // ; + @Override + public StringBuilder visitExclusiveOrExpression(CExpressionParser.ExclusiveOrExpressionContext ctx) { + if (ctx.andExpression().size() > 1) { + // andExpression ('^' andExpression)+ + // We have to abstract the xor expression if there is more than one operand + return ctx.accept(abstractingVisitor); + } else { + // andExpression + // There is exactly one child expression + return ctx.andExpression(0).accept(this); + } + } + + // inclusiveOrExpression + // : exclusiveOrExpression ('|' exclusiveOrExpression)* + // ; + @Override + public StringBuilder visitInclusiveOrExpression(CExpressionParser.InclusiveOrExpressionContext ctx) { + if (ctx.exclusiveOrExpression().size() > 1) { + // exclusiveOrExpression ('|' exclusiveOrExpression)+ + // We have to abstract the 'or' expression if there is more than one operand + return ctx.accept(abstractingVisitor); + } else { + // exclusiveOrExpression + // There is exactly one child expression + return ctx.exclusiveOrExpression(0).accept(this); + } + } + + // logicalAndExpression + // : logicalOperand ( '&&' logicalOperand)* + // ; + @Override + public StringBuilder visitLogicalAndExpression(CExpressionParser.LogicalAndExpressionContext ctx) { + return visitLogicalExpression(ctx, childExpression -> childExpression instanceof CExpressionParser.LogicalOperandContext); + } + + // logicalOrExpression + // : logicalAndExpression ( '||' logicalAndExpression)* + // ; + @Override + public StringBuilder visitLogicalOrExpression(CExpressionParser.LogicalOrExpressionContext ctx) { + return visitLogicalExpression(ctx, childExpression -> childExpression instanceof CExpressionParser.LogicalAndExpressionContext); + } + + // logicalOperand + // : inclusiveOrExpression + // ; + @Override + public StringBuilder visitLogicalOperand(CExpressionParser.LogicalOperandContext ctx) { + return ctx.inclusiveOrExpression().accept(this); + } + + private StringBuilder visitLogicalExpression(ParserRuleContext expressionContext, Function instanceCheck) { + StringBuilder sb = new StringBuilder(); + for (ParseTree subtree : expressionContext.children) { + if (instanceCheck.apply(subtree)) { + // logicalAndExpression | InclusiveOrExpression + sb.append(subtree.accept(this)); + } else if (subtree instanceof TerminalNode terminal) { + // '&&' | '||' + sb.append(terminal.getText().trim()); + } else { + // loop does not work as expected + throw new IllegalStateException(); + } + } + return sb; + } +} \ No newline at end of file diff --git a/src/main/java/org/variantsync/diffdetective/feature/jpp/AbstractingJPPExpressionVisitor.java b/src/main/java/org/variantsync/diffdetective/feature/jpp/AbstractingJPPExpressionVisitor.java new file mode 100644 index 000000000..af2bbe1fd --- /dev/null +++ b/src/main/java/org/variantsync/diffdetective/feature/jpp/AbstractingJPPExpressionVisitor.java @@ -0,0 +1,196 @@ +package org.variantsync.diffdetective.feature.jpp; + +import org.antlr.v4.runtime.ParserRuleContext; +import org.antlr.v4.runtime.tree.AbstractParseTreeVisitor; +import org.antlr.v4.runtime.tree.ParseTree; +import org.antlr.v4.runtime.tree.TerminalNode; +import org.variantsync.diffdetective.feature.BooleanAbstraction; +import org.variantsync.diffdetective.feature.antlr.JPPExpressionParser; +import org.variantsync.diffdetective.feature.antlr.JPPExpressionVisitor; + +import java.util.function.Function; + +public class AbstractingJPPExpressionVisitor extends AbstractParseTreeVisitor implements JPPExpressionVisitor { + // expression + // : logicalOrExpression + // ; + @Override + public StringBuilder visitExpression(JPPExpressionParser.ExpressionContext ctx) { + return ctx.logicalOrExpression().accept(this); + } + + // logicalOrExpression + // : logicalAndExpression (OR logicalAndExpression)* + // ; + @Override + public StringBuilder visitLogicalOrExpression(JPPExpressionParser.LogicalOrExpressionContext ctx) { + return visitLogicalExpression(ctx, + childExpression -> childExpression instanceof JPPExpressionParser.LogicalAndExpressionContext); + } + + // logicalAndExpression + // : primaryExpression (AND primaryExpression)* + // ; + @Override + public StringBuilder visitLogicalAndExpression(JPPExpressionParser.LogicalAndExpressionContext ctx) { + return visitLogicalExpression(ctx, + childExpression -> childExpression instanceof JPPExpressionParser.PrimaryExpressionContext); + } + + // primaryExpression + // : definedExpression + // | undefinedExpression + // | comparisonExpression + // ; + @Override + public StringBuilder visitPrimaryExpression(JPPExpressionParser.PrimaryExpressionContext ctx) { + if (ctx.definedExpression() != null) { + return ctx.definedExpression().accept(this); + } + if (ctx.undefinedExpression() != null) { + return ctx.undefinedExpression().accept(this); + } + if (ctx.comparisonExpression() != null) { + return ctx.comparisonExpression().accept(this); + } + throw new IllegalStateException("Unreachable code"); + } + + // comparisonExpression + // : operand ((LT|GT|LEQ|GEQ|EQ|NEQ) operand)? + // ; + @Override + public StringBuilder visitComparisonExpression(JPPExpressionParser.ComparisonExpressionContext ctx) { + return visitExpression(ctx, childExpression -> childExpression instanceof JPPExpressionParser.OperandContext); + } + + // operand + // : propertyExpression + // | Constant + // | StringLiteral+ + // | unaryOperator Constant + // ; + @Override + public StringBuilder visitOperand(JPPExpressionParser.OperandContext ctx) { + // propertyExpression + if (ctx.propertyExpression() != null) { + return ctx.propertyExpression().accept(this); + } + // unaryOperator Constant + if (ctx.unaryOperator() != null) { + StringBuilder sb = ctx.unaryOperator().accept(this); + sb.append(BooleanAbstraction.abstractAll(ctx.Constant().getText().trim())); + return sb; + } + // Constant + if (ctx.Constant() != null) { + return new StringBuilder(BooleanAbstraction.abstractAll(ctx.Constant().getText().trim())); + } + // StringLiteral+ + if (!ctx.StringLiteral().isEmpty()) { + StringBuilder sb = new StringBuilder(); + ctx.StringLiteral().stream().map(ParseTree::getText).map(String::trim).map(BooleanAbstraction::abstractAll).forEach(sb::append); + return sb; + } + // Unreachable + throw new IllegalStateException("Unreachable code."); + } + + // definedExpression + // : 'defined' '(' Identifier ')' + // ; + @Override + public StringBuilder visitDefinedExpression(JPPExpressionParser.DefinedExpressionContext ctx) { + StringBuilder sb = new StringBuilder("DEFINED_"); + sb.append(ctx.Identifier().getText().trim()); + return sb; + } + + // undefinedExpression + // : NOT 'defined' '(' Identifier ')' + // ; + @Override + public StringBuilder visitUndefinedExpression(JPPExpressionParser.UndefinedExpressionContext ctx) { + StringBuilder sb = new StringBuilder(); + sb.append(BooleanAbstraction.U_NOT); + sb.append("DEFINED_"); + sb.append(ctx.Identifier().getText().trim()); + return sb; + } + + // propertyExpression + // : '${' Identifier '}' + // ; + @Override + public StringBuilder visitPropertyExpression(JPPExpressionParser.PropertyExpressionContext ctx) { + return new StringBuilder(ctx.Identifier().getText().trim()); + } + + // unaryOperator + // : U_PLUS + // | U_MINUS + // ; + @Override + public StringBuilder visitUnaryOperator(JPPExpressionParser.UnaryOperatorContext ctx) { + switch (ctx.getText().trim()) { + case "+" -> { + return new StringBuilder(BooleanAbstraction.U_PLUS); + } + case "-" -> { + return new StringBuilder(BooleanAbstraction.U_MINUS); + } + } + throw new IllegalStateException("Unreachable code"); + } + + // logicalOrExpression + // : logicalAndExpression (OR logicalAndExpression)* + // ; + // logicalAndExpression + // : primaryExpression (AND primaryExpression)* + // ; + private StringBuilder visitLogicalExpression(ParserRuleContext expressionContext, Function instanceCheck) { + StringBuilder sb = new StringBuilder(); + for (ParseTree subtree : expressionContext.children) { + if (instanceCheck.apply(subtree)) { + // logicalAndExpression | InclusiveOrExpression + sb.append(subtree.accept(this)); + } else if (subtree instanceof TerminalNode terminal) { + // '&&' | '||' + switch (subtree.getText()) { + case "and" -> sb.append("&&"); + case "or" -> sb.append("||"); + default -> throw new IllegalStateException(); + } + } else { + // loop does not work as expected + throw new IllegalStateException(); + } + } + return sb; + } + + /** + * Abstract all child nodes in the parse tree. + * + * @param expressionContext The root of the subtree to abstract + * @param instanceCheck A check for expected child node types + * @return The abstracted formula of the subtree + */ + private StringBuilder visitExpression(ParserRuleContext expressionContext, Function instanceCheck) { + StringBuilder sb = new StringBuilder(); + for (ParseTree subtree : expressionContext.children) { + if (instanceCheck.apply(subtree)) { + // Some operand (i.e., a subtree) that we have to visit + sb.append(subtree.accept(this)); + } else if (subtree instanceof TerminalNode terminal) { + // Some operator (i.e., a leaf node) that requires direct abstraction + sb.append(BooleanAbstraction.abstractToken(terminal.getText().trim())); + } else { + // sanity check: loop does not work as expected + throw new IllegalStateException(); + } + } + return sb; + } +} diff --git a/src/main/java/org/variantsync/diffdetective/feature/jpp/JPPDiffLineFormulaExtractor.java b/src/main/java/org/variantsync/diffdetective/feature/jpp/JPPDiffLineFormulaExtractor.java new file mode 100644 index 000000000..f760cd8ec --- /dev/null +++ b/src/main/java/org/variantsync/diffdetective/feature/jpp/JPPDiffLineFormulaExtractor.java @@ -0,0 +1,49 @@ +package org.variantsync.diffdetective.feature.jpp; + +import org.antlr.v4.runtime.CharStreams; +import org.antlr.v4.runtime.CommonTokenStream; +import org.antlr.v4.runtime.tree.ParseTree; +import org.variantsync.diffdetective.feature.AbstractingFormulaExtractor; +import org.variantsync.diffdetective.feature.ParseErrorListener; +import org.variantsync.diffdetective.feature.antlr.JPPExpressionLexer; +import org.variantsync.diffdetective.feature.antlr.JPPExpressionParser; + +import java.util.regex.Pattern; + +/** + * Extracts the expression from a JavaPP (Java PreProcessor) statement . + * For example, given the annotation "//#if defined(A) || B()", the extractor would extract "DEFINED_A || B". + * The extractor detects if and elif annotations (other annotations do not have expressions). + * The given JPP statement might also be a line in a diff (i.e., preceeded by a - or +). + * + * @author Alexander Schultheiß + */ +public class JPPDiffLineFormulaExtractor extends AbstractingFormulaExtractor { + private static final String JPP_ANNOTATION_REGEX = "^[+-]?\\s*//\\s*#\\s*(if|elif)(\\s+(.*)|(\\(.*\\)))$"; + private static final Pattern JPP_ANNOTATION_PATTERN = Pattern.compile(JPP_ANNOTATION_REGEX); + + public JPPDiffLineFormulaExtractor() { + super(JPP_ANNOTATION_PATTERN); + } + + /** + * Abstract the given formula. + *

+ * First, the visitor uses ANTLR to parse the formula into a parse tree gives the tree to a {@link AbstractingJPPExpressionVisitor}. + * The visitor traverses the tree starting from the root, searching for subtrees that must be abstracted. + * If such a subtree is found, the visitor abstracts the part of the formula in the subtree. + *

+ * + * @param formula that is to be abstracted + * @return the abstracted formula + */ + @Override + protected String abstractFormula(String formula) { + JPPExpressionLexer lexer = new JPPExpressionLexer(CharStreams.fromString(formula)); + CommonTokenStream tokens = new CommonTokenStream(lexer); + JPPExpressionParser parser = new JPPExpressionParser(tokens); + parser.addErrorListener(new ParseErrorListener(formula)); + ParseTree tree = parser.expression(); + return tree.accept(new AbstractingJPPExpressionVisitor()).toString(); + } +} diff --git a/src/main/java/org/variantsync/diffdetective/internal/SimpleRenderer.java b/src/main/java/org/variantsync/diffdetective/internal/SimpleRenderer.java index 64854a9ec..e27983cc1 100644 --- a/src/main/java/org/variantsync/diffdetective/internal/SimpleRenderer.java +++ b/src/main/java/org/variantsync/diffdetective/internal/SimpleRenderer.java @@ -5,18 +5,18 @@ import org.variantsync.diffdetective.datasets.Repository; import org.variantsync.diffdetective.diff.git.PatchDiff; import org.variantsync.diffdetective.diff.result.DiffParseException; -import org.variantsync.diffdetective.feature.CPPAnnotationParser; -import org.variantsync.diffdetective.mining.VariationDiffMiner; +import org.variantsync.diffdetective.feature.PreprocessorAnnotationParser; import org.variantsync.diffdetective.mining.RWCompositePatternNodeFormat; import org.variantsync.diffdetective.mining.RWCompositePatternTreeFormat; +import org.variantsync.diffdetective.mining.VariationDiffMiner; import org.variantsync.diffdetective.util.Assert; import org.variantsync.diffdetective.util.FileUtils; import org.variantsync.diffdetective.variation.DiffLinesLabel; import org.variantsync.diffdetective.variation.diff.VariationDiff; import org.variantsync.diffdetective.variation.diff.parse.VariationDiffParseOptions; import org.variantsync.diffdetective.variation.diff.parse.VariationDiffParser; -import org.variantsync.diffdetective.variation.diff.render.VariationDiffRenderer; import org.variantsync.diffdetective.variation.diff.render.RenderOptions; +import org.variantsync.diffdetective.variation.diff.render.VariationDiffRenderer; import org.variantsync.diffdetective.variation.diff.serialize.nodeformat.MappingsDiffNodeFormat; import org.variantsync.diffdetective.variation.diff.transform.VariationDiffTransformer; @@ -33,6 +33,7 @@ * directory. * This class is mostly used for debuggin purposes within DiffDetective and * contains mostly quick-and-dirty hardcoded configuration options. + * * @author Paul Bittner */ public class SimpleRenderer { @@ -41,9 +42,9 @@ public class SimpleRenderer { // .setNodeFormat(new ReleaseMiningDiffNodeFormat()), .setNodeFormat(new MappingsDiffNodeFormat<>()) .setDpi(RenderOptions.DEFAULT().dpi() / 2) - .setNodesize(3*RenderOptions.DEFAULT().nodesize()) - .setEdgesize(2*RenderOptions.DEFAULT().edgesize()) - .setArrowsize(2*RenderOptions.DEFAULT().arrowsize()) + .setNodesize(3 * RenderOptions.DEFAULT().nodesize()) + .setEdgesize(2 * RenderOptions.DEFAULT().edgesize()) + .setArrowsize(2 * RenderOptions.DEFAULT().arrowsize()) .setFontsize(8) // .addExtraArguments("--format", "patternsrelease") .setCleanUpTemporaryFiles(false) @@ -63,18 +64,18 @@ public class SimpleRenderer { private static final RenderOptions renderExampleOptions = new RenderOptions.Builder() .setTreeFormat(new RWCompositePatternTreeFormat()) - .setNodesize(3*RenderOptions.DEFAULT().nodesize()) - .setEdgesize(2*RenderOptions.DEFAULT().edgesize()) - .setArrowsize(2*RenderOptions.DEFAULT().arrowsize()) + .setNodesize(3 * RenderOptions.DEFAULT().nodesize()) + .setEdgesize(2 * RenderOptions.DEFAULT().edgesize()) + .setArrowsize(2 * RenderOptions.DEFAULT().arrowsize()) .setFontsize(8) .addExtraArguments("--startlineno", "4201") .build(); private static final RenderOptions renderCompositePatterns = new RenderOptions.Builder() - .setNodesize(3*RenderOptions.DEFAULT().nodesize()) - .setEdgesize(2*RenderOptions.DEFAULT().edgesize()) - .setArrowsize(2*RenderOptions.DEFAULT().arrowsize()) - .setFontsize(2*RenderOptions.DEFAULT().fontsize()) + .setNodesize(3 * RenderOptions.DEFAULT().nodesize()) + .setEdgesize(2 * RenderOptions.DEFAULT().edgesize()) + .setArrowsize(2 * RenderOptions.DEFAULT().arrowsize()) + .setFontsize(2 * RenderOptions.DEFAULT().fontsize()) .setTreeFormat(new RWCompositePatternTreeFormat()) .setNodeFormat(new RWCompositePatternNodeFormat()) .setCleanUpTemporaryFiles(true) @@ -89,8 +90,7 @@ public class SimpleRenderer { private final static Function GetRelativeOutputDir = // Path::getParent - p -> p.getParent().resolve("render") - ; + p -> p.getParent().resolve("render"); private static void render(final Path fileToRender) { if (FileUtils.isLineGraph(fileToRender)) { @@ -102,7 +102,7 @@ private static void render(final Path fileToRender) { try { t = VariationDiff.fromFile(fileToRender, new VariationDiffParseOptions( - CPPAnnotationParser.Default, collapseMultipleCodeLines, ignoreEmptyLines + PreprocessorAnnotationParser.CPPAnnotationParser, collapseMultipleCodeLines, ignoreEmptyLines )); } catch (IOException | DiffParseException e) { Logger.error(e, "Could not read given file '{}'", fileToRender); @@ -125,9 +125,10 @@ private static void render(final Path fileToRender) { * Expects one of the following argument configurations. * 1.) For rendering files: Exactly one argument pointing to a file or directory to render. * 2.) For rendering patches: Exactly three arguments. - * The first argument is the path to a local directory from which a patch should be analyzed. - * The second argument is a commit hash. - * The third argument is the file name of the patched file in the given commit. + * The first argument is the path to a local directory from which a patch should be analyzed. + * The second argument is a commit hash. + * The third argument is the file name of the patched file in the given commit. + * * @param args See above * @throws IOException when reading a file fails. */ diff --git a/src/main/java/org/variantsync/diffdetective/variation/NodeType.java b/src/main/java/org/variantsync/diffdetective/variation/NodeType.java index 780027560..b60738c2a 100644 --- a/src/main/java/org/variantsync/diffdetective/variation/NodeType.java +++ b/src/main/java/org/variantsync/diffdetective/variation/NodeType.java @@ -1,6 +1,7 @@ package org.variantsync.diffdetective.variation; -import org.variantsync.diffdetective.variation.diff.DiffNode; // For Javadoc +import org.variantsync.diffdetective.feature.AnnotationType; +import org.variantsync.diffdetective.variation.diff.DiffNode; import org.variantsync.diffdetective.variation.tree.VariationNode; // For Javadoc /** @@ -17,12 +18,14 @@ public enum NodeType { ARTIFACT("artifact"); public final String name; + NodeType(String name) { this.name = name; } /** - * Returns true iff this node type represents a conditional feature annotation (i.e., if or elif). + * Returns true iff this node type represents a conditional feature annotation + * (i.e., if or elif). */ public boolean isConditionalAnnotation() { return this == IF || this == ELIF; @@ -37,9 +40,11 @@ public boolean isAnnotation() { /** * Creates a NodeType from its value names. - * @see Enum#name() - * @param name a string that equals the name of one value of this enum (ignoring case) + * + * @param name a string that equals the name of one value of this enum (ignoring + * case) * @return The NodeType that has the given name + * @see Enum#name() */ public static NodeType fromName(final String name) { for (NodeType candidate : values()) { @@ -52,7 +57,28 @@ public static NodeType fromName(final String name) { } /** - * Returns the number of bits required for storing {@link ordinal}. + * Creates a NodeType from an AnnotationType. + *

+ * All AnnotationType variants except for 'Endif' are supported. + * There is no valid representation for 'Endif' annotations. Thus, the method throws an IllegalArgumentException + * if it is given an 'Endif'. + *

+ * + * @param annotationType a variant of AnnotationType + * @return The NodeType that fits the given AnnotationType + */ + public static NodeType fromAnnotationType(final AnnotationType annotationType) { + return switch (annotationType) { + case If -> NodeType.IF; + case Elif -> NodeType.ELIF; + case Else -> NodeType.ELSE; + case None -> NodeType.ARTIFACT; + case Endif -> throw new IllegalArgumentException(annotationType + "has no NodeType counterpart"); + }; + } + + /** + * Returns the number of bits required for storing. */ public static int getRequiredBitCount() { return 3; diff --git a/src/main/java/org/variantsync/diffdetective/variation/diff/parse/VariationDiffParseOptions.java b/src/main/java/org/variantsync/diffdetective/variation/diff/parse/VariationDiffParseOptions.java index 4f0d600dc..1269e9463 100644 --- a/src/main/java/org/variantsync/diffdetective/variation/diff/parse/VariationDiffParseOptions.java +++ b/src/main/java/org/variantsync/diffdetective/variation/diff/parse/VariationDiffParseOptions.java @@ -1,22 +1,25 @@ package org.variantsync.diffdetective.variation.diff.parse; -import org.variantsync.diffdetective.feature.CPPAnnotationParser; +import org.variantsync.diffdetective.feature.AnnotationParser; +import org.variantsync.diffdetective.feature.PreprocessorAnnotationParser; /** * Parse options that should be used when parsing {@link org.variantsync.diffdetective.variation.diff.VariationDiff}s. - * @param annotationParser A parser for parsing c preprocessor annotations. + * + * @param annotationParser A parser for parsing annotations. * @param collapseMultipleCodeLines Whether multiple consecutive code lines with the same diff - * type should be collapsed into a single artifact node. - * @param ignoreEmptyLines Whether to add {@code DiffNode}s for empty lines (regardless of their {@code DiffType}). - * If {@link #collapseMultipleCodeLines} is {@code true} empty lines are also not added to - * existing {@code DiffNode}s. + * type should be collapsed into a single artifact node. + * @param ignoreEmptyLines Whether to add {@code DiffNode}s for empty lines (regardless of their {@code DiffType}). + * If {@link #collapseMultipleCodeLines} is {@code true} empty lines are also not added to + * existing {@code DiffNode}s. * @author Paul Bittner */ public record VariationDiffParseOptions( - CPPAnnotationParser annotationParser, + AnnotationParser annotationParser, boolean collapseMultipleCodeLines, boolean ignoreEmptyLines ) { + /** * Creates VariationDiffParseOptions with the default parser as specified in {@link #Default}. */ @@ -34,7 +37,7 @@ public VariationDiffParseOptions( /** * Creates VariationDiffParseOptions with the given annotation parser. */ - public VariationDiffParseOptions withAnnotationParser(CPPAnnotationParser annotationParser) { + public VariationDiffParseOptions withAnnotationParser(AnnotationParser annotationParser) { return new VariationDiffParseOptions( annotationParser, this.collapseMultipleCodeLines(), @@ -44,10 +47,10 @@ public VariationDiffParseOptions withAnnotationParser(CPPAnnotationParser annota /** * Default value for VariationDiffParseOptions that does not remember parsed unix diffs - * and uses the default value for the parsing annotations ({@link CPPAnnotationParser#Default}). + * and uses the default value for the parsing annotations ({@link PreprocessorAnnotationParser#CPPAnnotationParser}). */ public static final VariationDiffParseOptions Default = new VariationDiffParseOptions( - CPPAnnotationParser.Default, + PreprocessorAnnotationParser.CPPAnnotationParser, false, false ); diff --git a/src/main/java/org/variantsync/diffdetective/variation/diff/parse/VariationDiffParser.java b/src/main/java/org/variantsync/diffdetective/variation/diff/parse/VariationDiffParser.java index b8cb4cdfa..01863a8c0 100644 --- a/src/main/java/org/variantsync/diffdetective/variation/diff/parse/VariationDiffParser.java +++ b/src/main/java/org/variantsync/diffdetective/variation/diff/parse/VariationDiffParser.java @@ -14,19 +14,19 @@ import org.variantsync.diffdetective.diff.result.DiffParseException; import org.variantsync.diffdetective.diff.text.DiffLineNumber; import org.variantsync.diffdetective.error.UnparseableFormulaException; +import org.variantsync.diffdetective.feature.AnnotationType; import org.variantsync.diffdetective.util.Assert; import org.variantsync.diffdetective.variation.DiffLinesLabel; import org.variantsync.diffdetective.variation.NodeType; import org.variantsync.diffdetective.variation.diff.DiffNode; -import org.variantsync.diffdetective.variation.diff.VariationDiff; import org.variantsync.diffdetective.variation.diff.DiffType; import org.variantsync.diffdetective.variation.diff.Time; +import org.variantsync.diffdetective.variation.diff.VariationDiff; import java.io.BufferedReader; import java.io.IOException; import java.io.StringReader; import java.util.Stack; -import java.util.regex.Pattern; /** * Parser that parses {@link VariationDiff}s from text-based diffs. @@ -34,20 +34,28 @@ * Note: Weird line continuations and comments can cause misidentification of conditional macros. * The following examples are all correct according to the C11 standard: (comment end is marked by * {@code *\/}): + * + *

* * /* * #ifdef A * *\/ - * + * + *

+ * * #ifdef /* * *\/ A * #endif - * + * + *

+ * * # /**\/ ifdef * #endif - * + * + *

+ * * # \ - * ifdef + * ifdef * #endif * */ @@ -58,20 +66,11 @@ public class VariationDiffParser { * instead of some source code file. * * @param diffType the diff type of this line, may be {@code null} if this line has no valid - * diff type - * @param content the actual line content without a line delimiter + * diff type + * @param content the actual line content without a line delimiter */ - public record DiffLine(DiffType diffType, String content) {} - - /** - * Matches the beginning of conditional macros. - * It doesn't match the whole macro name, for example for {@code #ifdef} only {@code "#if"} is - * matched and only {@code "if"} is captured. - *

- * Note that this pattern doesn't handle comments between {@code #} and the macro name. - */ - private final static Pattern macroPattern = - Pattern.compile("^[+-]?\\s*#\\s*(if|elif|else|endif)"); + public record DiffLine(DiffType diffType, String content) { + } /* Settings */ @@ -131,9 +130,9 @@ public static VariationDiff createVariationDiff( * This parsing algorithm is described in detail in Sören Viegener's bachelor's thesis. * * @param fullDiff The full diff of a patch obtained from a buffered reader. - * @param options {@link VariationDiffParseOptions} for the parsing process. + * @param options {@link VariationDiffParseOptions} for the parsing process. * @return A parsed {@link VariationDiff} upon success or an error indicating why parsing failed. - * @throws IOException when reading from {@code fullDiff} fails. + * @throws IOException when reading from {@code fullDiff} fails. * @throws DiffParseException if an error in the diff or macro syntax is detected */ public static VariationDiff createVariationDiff( @@ -141,7 +140,7 @@ public static VariationDiff createVariationDiff( final VariationDiffParseOptions options ) throws IOException, DiffParseException { return new VariationDiffParser( - options + options ).parse(() -> { String line = fullDiff.readLine(); if (line == null) { @@ -156,10 +155,10 @@ public static VariationDiff createVariationDiff( * This method is similar to {@link #createVariationDiff(BufferedReader, VariationDiffParseOptions)} * but acts as if all lines where unmodified. * - * @param file The source code file (not a diff) to be parsed. + * @param file The source code file (not a diff) to be parsed. * @param options {@link VariationDiffParseOptions} for the parsing process. * @return A parsed {@link VariationDiff}. - * @throws IOException iff {@code file} throws an {@code IOException} + * @throws IOException iff {@code file} throws an {@code IOException} * @throws DiffParseException if an error in the diff or macro syntax is detected */ public static VariationDiff createVariationTree( @@ -167,7 +166,7 @@ public static VariationDiff createVariationTree( VariationDiffParseOptions options ) throws IOException, DiffParseException { return new VariationDiffParser( - options + options ).parse(() -> { String line = file.readLine(); if (line == null) { @@ -175,9 +174,9 @@ public static VariationDiff createVariationTree( } else { if (line.startsWith("+") || line.startsWith("-")) { Logger.warn( - "The source file given to createVariationTree contains a plus or " + - "minus sign at the start of a line. Please ensure that you are " + - "actually parsing a source file and not a diff." + "The source file given to createVariationTree contains a plus or " + + "minus sign at the start of a line. Please ensure that you are " + + "actually parsing a source file and not a diff." ); } @@ -201,14 +200,14 @@ private VariationDiffParser( * Parses the line diff {@code fullDiff}. * * @param lines should supply successive lines of the diff to be parsed, or {@code null} if - * there are no more lines to be parsed. + * there are no more lines to be parsed. * @return the parsed {@code VariationDiff} - * @throws IOException iff {@code lines.get()} throws {@code IOException} + * @throws IOException iff {@code lines.get()} throws {@code IOException} * @throws DiffParseException if an error in the line diff or the underlying preprocessor syntax - * is detected + * is detected */ private VariationDiff parse( - FailableSupplier lines + FailableSupplier lines ) throws IOException, DiffParseException { DiffNode root = DiffNode.createRoot(new DiffLinesLabel()); beforeStack.push(root); @@ -238,12 +237,12 @@ private VariationDiff parse( // Do beforeLine and afterLine represent the same unchanged diff line? isNon = diffType == DiffType.NON && - (isNon || (!beforeLine.hasStarted() && !afterLine.hasStarted())); + (isNon || (!beforeLine.hasStarted() && !afterLine.hasStarted())); // Add the physical line to the logical line. final DiffLineNumber lineNumberFinal = lineNumber; diffType.forAllTimesOfExistence(beforeLine, afterLine, - node -> node.consume(currentLine, lineNumberFinal) + node -> node.consume(currentLine, lineNumberFinal) ); // Parse the completed logical line @@ -270,21 +269,21 @@ private VariationDiff parse( Logger.debug("beforeLine: " + beforeLine); Logger.debug("afterLine: " + afterLine); throw new DiffParseException( - DiffError.INVALID_LINE_CONTINUATION, - lineNumber + DiffError.INVALID_LINE_CONTINUATION, + lineNumber ); } if (beforeStack.size() > 1) { throw new DiffParseException( - DiffError.NOT_ALL_ANNOTATIONS_CLOSED, - beforeStack.peek().getFromLine() + DiffError.NOT_ALL_ANNOTATIONS_CLOSED, + beforeStack.peek().getFromLine() ); } if (afterStack.size() > 1) { throw new DiffParseException( - DiffError.NOT_ALL_ANNOTATIONS_CLOSED, - afterStack.peek().getFromLine() + DiffError.NOT_ALL_ANNOTATIONS_CLOSED, + afterStack.peek().getFromLine() ); } @@ -299,8 +298,8 @@ private VariationDiff parse( /** * Parses one logical line and most notably, handles conditional macros. * - * @param line a logical line with {@code line.isComplete() == true} - * @param diffType whether {@code line} was added, inserted or unchanged + * @param line a logical line with {@code line.isComplete() == true} + * @param diffType whether {@code line} was added, inserted or unchanged * @param lastLineNumber the last physical line of {@code line} * @throws DiffParseException if erroneous preprocessor macros are detected */ @@ -314,21 +313,18 @@ private void parseLine( // Is this line a conditional macro? // Note: The following line doesn't handle comments and line continuations correctly. - var matcher = macroPattern.matcher(line.toString()); - var conditionalMacroName = matcher.find() - ? matcher.group(1) - : null; + var annotationType = options.annotationParser().determineAnnotationType(line.toString()); - if ("endif".equals(conditionalMacroName)) { + if (annotationType == AnnotationType.Endif) { lastArtifact = null; // Do not create a node for ENDIF, but update the line numbers of the closed if-chain // and remove that if-chain from the relevant stacks. diffType.forAllTimesOfExistence(beforeStack, afterStack, stack -> - popIfChain(stack, fromLine) + popIfChain(stack, fromLine) ); } else if (options.collapseMultipleCodeLines() - && conditionalMacroName == null + && annotationType == AnnotationType.None && lastArtifact != null && lastArtifact.diffType.equals(diffType) && lastArtifact.getToLine().inDiff() == fromLine.inDiff()) { @@ -337,24 +333,17 @@ private void parseLine( lastArtifact.setToLine(toLine); } else { try { - NodeType nodeType = NodeType.ARTIFACT; - if (conditionalMacroName != null) { - try { - nodeType = NodeType.fromName(conditionalMacroName); - } catch (IllegalArgumentException e) { - throw new DiffParseException(DiffError.INVALID_MACRO_NAME, fromLine); - } - } + NodeType nodeType = NodeType.fromAnnotationType(annotationType); DiffNode newNode = new DiffNode( - diffType, - nodeType, - fromLine, - toLine, - nodeType == NodeType.ARTIFACT || nodeType == NodeType.ELSE - ? null - : options.annotationParser().parseDiffLine(line.toString()), - new DiffLinesLabel(line.getLines()) + diffType, + nodeType, + fromLine, + toLine, + nodeType == NodeType.ARTIFACT || nodeType == NodeType.ELSE + ? null + : options.annotationParser().parseAnnotation(line.toString()), + new DiffLinesLabel(line.getLines()) ); addNode(newNode); @@ -370,13 +359,13 @@ private void parseLine( * If there were ELSEs or ELIFs between an IF and an ENDIF, they were placed on the stack and * have to be popped now. The {@link DiffNode#getToLine() end line numbers} are adjusted * - * @param stack the stack which should be popped + * @param stack the stack which should be popped * @param elseLineNumber the first line of the else which causes this IF to be popped * @throws DiffParseException if {@code stack} doesn't contain an IF node */ private void popIfChain( - Stack> stack, - DiffLineNumber elseLineNumber + Stack> stack, + DiffLineNumber elseLineNumber ) throws DiffParseException { DiffLineNumber previousLineNumber = elseLineNumber; do { @@ -385,13 +374,13 @@ private void popIfChain( // Set the line number of now closed annotations to the beginning of the // following annotation. annotation.setToLine(new DiffLineNumber( - Math.max(previousLineNumber.inDiff(), annotation.getToLine().inDiff()), - stack == beforeStack - ? previousLineNumber.beforeEdit() - : annotation.getToLine().beforeEdit(), - stack == afterStack - ? previousLineNumber.afterEdit() - : annotation.getToLine().afterEdit() + Math.max(previousLineNumber.inDiff(), annotation.getToLine().inDiff()), + stack == beforeStack + ? previousLineNumber.beforeEdit() + : annotation.getToLine().beforeEdit(), + stack == afterStack + ? previousLineNumber.afterEdit() + : annotation.getToLine().afterEdit() )); previousLineNumber = annotation.getFromLine(); @@ -418,8 +407,8 @@ private void addNode(DiffNode newNode) throws DiffParseException if (newNode.isElif() || newNode.isElse()) { if (stack.size() == 1) { throw new DiffParseException( - DiffError.ELSE_OR_ELIF_WITHOUT_IF, - newNode.getFromLine() + DiffError.ELSE_OR_ELIF_WITHOUT_IF, + newNode.getFromLine() ); } @@ -435,7 +424,8 @@ private void addNode(DiffNode newNode) throws DiffParseException /** * Parses the given commit of the given repository. - * @param repo The repository from which a commit should be parsed. + * + * @param repo The repository from which a commit should be parsed. * @param commitHash Hash of the commit to parse. * @return A CommitDiff describing edits to variability introduced by the given commit relative * to its first parent commit. @@ -463,12 +453,13 @@ public static CommitDiff parseCommit(Repository repo, String commitHash) throws /** * Parses the given patch of the given repository. - * @param repo The repository from which a patch should be parsed. - * @param file The file that was edited by the patch. + * + * @param repo The repository from which a patch should be parsed. + * @param file The file that was edited by the patch. * @param commitHash The hash of the commit in which the patch was made. * @return A PatchDiff describing edits to variability introduced by the given patch relative to * the corresponding commit's first parent commit. - * @throws IOException when an error occurred. + * @throws IOException when an error occurred. * @throws AssertionError when no such patch exists. */ public static PatchDiff parsePatch(Repository repo, String file, String commitHash) throws IOException { diff --git a/src/main/java/org/variantsync/diffdetective/variation/tree/VariationTree.java b/src/main/java/org/variantsync/diffdetective/variation/tree/VariationTree.java index c53bfbc37..36cdff174 100644 --- a/src/main/java/org/variantsync/diffdetective/variation/tree/VariationTree.java +++ b/src/main/java/org/variantsync/diffdetective/variation/tree/VariationTree.java @@ -54,6 +54,14 @@ public VariationTree(VariationTreeNode root, VariationTreeSource source) { Assert.assertTrue(root.isRoot()); } + /** + * Same as {@link #fromFile(Path, VariationDiffParseOptions)} + * but with {@link VariationDiffParseOptions#Default} parse options. + */ + public static VariationTree fromFile(final Path path) throws IOException, DiffParseException { + return fromFile(path, VariationDiffParseOptions.Default); + } + /** * Same as {@link #fromFile(BufferedReader, VariationTreeSource, VariationDiffParseOptions)} * but registers {@code path} as source. diff --git a/src/test/java/CPPParserTest.java b/src/test/java/CPPParserTest.java index 77b6ed19a..3756098bd 100644 --- a/src/test/java/CPPParserTest.java +++ b/src/test/java/CPPParserTest.java @@ -1,135 +1,138 @@ -import org.variantsync.diffdetective.error.UnparseableFormulaException; -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 org.variantsync.diffdetective.error.UnparseableFormulaException; +import org.variantsync.diffdetective.feature.cpp.CPPDiffLineFormulaExtractor; + +import java.util.List; 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 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 A && B || C", "A&&B||C"), - - new TestCase("#if 1 > -42", "1__GT____U_MINUS__42"), - new TestCase("#if 1 > +42", "1__GT____U_PLUS__42"), - new TestCase("#if 42 > A", "42__GT__A"), - new TestCase("#if 42 > ~A", "42__GT____U_TILDE__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__COLON__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", "DEFINED___LB__A__RB__&&__LB__B__MUL__2__RB____GT__C"), - new TestCase("#if(STDC == 1) && (defined(LARGE) || defined(COMPACT))", "(STDC__EQ__1)&&(DEFINED___LB__LARGE__RB__||DEFINED___LB__COMPACT__RB__)"), - new TestCase("#if (('Z' - 'A') == 25)", "(__LB____SQUOTE__Z__SQUOTE____SUB____SQUOTE__A__SQUOTE____RB____EQ__25)"), - new TestCase("#if APR_CHARSET_EBCDIC && !(('Z' - 'A') == 25)", "APR_CHARSET_EBCDIC&&!(__LB____SQUOTE__Z__SQUOTE____SUB____SQUOTE__A__SQUOTE____RB____EQ__25)"), - new TestCase("# if ((GNUTLS_VERSION_MAJOR + (GNUTLS_VERSION_MINOR > 0 || GNUTLS_VERSION_PATCH >= 20)) > 3)", - "(__LB__GNUTLS_VERSION_MAJOR__ADD____LB__GNUTLS_VERSION_MINOR__GT__0__L_OR__GNUTLS_VERSION_PATCH__GEQ__20__RB____RB____GT__3)"), - - new TestCase("#if A && (B > C)", "A&&(B__GT__C)"), - new TestCase("#if (A && B) > C", "__LB__A__L_AND__B__RB____GT__C"), - new TestCase("#if C == (A || B)", "C__EQ____LB__A__L_OR__B__RB__"), - new TestCase("#if ((A && B) > C)", "(__LB__A__L_AND__B__RB____GT__C)"), - new TestCase("#if A && ((B + 1) > (C || D))", "A&&(__LB__B__ADD__1__RB____GT____LB__C__L_OR__D__RB__)"), - - new TestCase("#if __has_include", "HAS_INCLUDE_"), - new TestCase("#if defined __has_include", "DEFINED_HAS_INCLUDE_"), - new TestCase("#if __has_include()", "HAS_INCLUDE___LB____LT__nss3__DIV__nss__DOT__h__GT____RB__"), - new TestCase("#if __has_include()", "HAS_INCLUDE___LB____LT__nss__DOT__h__GT____RB__"), - new TestCase("#if __has_include(\"nss3/nss.h\")", "HAS_INCLUDE___LB____QUOTE__nss3__DIV__nss__DOT__h__QUOTE____RB__"), - new TestCase("#if __has_include(\"nss.h\")", "HAS_INCLUDE___LB____QUOTE__nss__DOT__h__QUOTE____RB__"), - - new TestCase("#if __has_attribute", "HAS_ATTRIBUTE_"), - new TestCase("#if defined __has_attribute", "DEFINED_HAS_ATTRIBUTE_"), - new TestCase("# if __has_attribute (nonnull)", "HAS_ATTRIBUTE___LB__nonnull__RB__"), - new TestCase("#if defined __has_attribute && __has_attribute (nonnull)", "DEFINED_HAS_ATTRIBUTE_&&HAS_ATTRIBUTE___LB__nonnull__RB__"), - - new TestCase("#if __has_cpp_attribute", "HAS_CPP_ATTRIBUTE_"), - new TestCase("#if defined __has_cpp_attribute", "DEFINED_HAS_CPP_ATTRIBUTE_"), - new TestCase("#if __has_cpp_attribute (nonnull)", "HAS_CPP_ATTRIBUTE___LB__nonnull__RB__"), - new TestCase("#if __has_cpp_attribute (nonnull) && A", "HAS_CPP_ATTRIBUTE___LB__nonnull__RB__&&A"), - - new TestCase("#if defined __has_c_attribute", "DEFINED_HAS_C_ATTRIBUTE_"), - new TestCase("#if __has_c_attribute", "HAS_C_ATTRIBUTE_"), - new TestCase("#if __has_c_attribute (nonnull)", "HAS_C_ATTRIBUTE___LB__nonnull__RB__"), - new TestCase("#if __has_c_attribute (nonnull) && A", "HAS_C_ATTRIBUTE___LB__nonnull__RB__&&A"), - - new TestCase("#if defined __has_builtin", "DEFINED_HAS_BUILTIN_"), - new TestCase("#if __has_builtin", "HAS_BUILTIN_"), - new TestCase("#if __has_builtin (__nonnull)", "HAS_BUILTIN___LB____nonnull__RB__"), - new TestCase("#if __has_builtin (nonnull) && A", "HAS_BUILTIN___LB__nonnull__RB__&&A"), - - new TestCase("#if A // Comment && B", "A"), - new TestCase("#if A /* Comment */ && B", "A&&B"), - new TestCase("#if A && B /* Multiline Comment", "A&&B"), - - new TestCase("#if A == B", "A__EQ__B"), - new TestCase("#if A == 1", "A__EQ__1"), - - new TestCase("#if defined A", "DEFINED_A"), - new TestCase("#if defined(A)", "DEFINED___LB__A__RB__"), - new TestCase("#if defined (A)", "DEFINED___LB__A__RB__"), - new TestCase("#if defined ( A )", "DEFINED___LB__A__RB__"), - new TestCase("#if (defined A)", "(DEFINED_A)"), - 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"), - - new TestCase("#if __has_include_next()", "__HAS_INCLUDE_NEXT___LB____LT__some__SUB__header__DOT__h__GT____RB__"), - new TestCase("#if __is_target_arch(x86)", "__IS_TARGET_ARCH___LB__x86__RB__"), - new TestCase("#if A || (defined(NAME) && (NAME >= 199630))", "A||(DEFINED___LB__NAME__RB__&&(NAME__GEQ__199630))"), - new TestCase("#if MACRO(part:part)", "MACRO___LB__part__COLON__part__RB__"), - new TestCase("#if MACRO(x=1)", "MACRO___LB__x__ASSIGN__1__RB__"), - new TestCase("#if A = 3", "A__ASSIGN__3"), - new TestCase("#if ' ' == 32", "__SQUOTE_____SQUOTE____EQ__32"), - new TestCase("#if (NAME<<1) > (1<= 199905) && (NAME < 1991011)) || (NAME >= 300000) || defined(NAME)", "(DEFINED___LB__NAME__RB__&&(NAME__GEQ__199905)&&(NAME__LT__1991011))||(NAME__GEQ__300000)||DEFINED___LB__NAME__RB__"), - new TestCase("#if __has_warning(\"-Wa-warning\"_foo)", - "__HAS_WARNING___LB____QUOTE____SUB__Wa__SUB__warning__QUOTE_____foo__RB__") + 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 A && B || C", "A&&B||C"), + + new TestCase("#if 1 > -42", "1__GT____U_MINUS__42"), + new TestCase("#if 1 > +42", "1__GT____U_PLUS__42"), + new TestCase("#if 42 > A", "42__GT__A"), + new TestCase("#if 42 > ~A", "42__GT____U_TILDE__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__COLON__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", "DEFINED___LB__A__RB__&&__LB__B__MUL__2__RB____GT__C"), + new TestCase("#if(STDC == 1) && (defined(LARGE) || defined(COMPACT))", "(STDC__EQ__1)&&(DEFINED___LB__LARGE__RB__||DEFINED___LB__COMPACT__RB__)"), + new TestCase("#if (('Z' - 'A') == 25)", "(__LB____SQUOTE__Z__SQUOTE____SUB____SQUOTE__A__SQUOTE____RB____EQ__25)"), + new TestCase("#if APR_CHARSET_EBCDIC && !(('Z' - 'A') == 25)", "APR_CHARSET_EBCDIC&&!(__LB____SQUOTE__Z__SQUOTE____SUB____SQUOTE__A__SQUOTE____RB____EQ__25)"), + new TestCase("# if ((GNUTLS_VERSION_MAJOR + (GNUTLS_VERSION_MINOR > 0 || GNUTLS_VERSION_PATCH >= 20)) > 3)", + "(__LB__GNUTLS_VERSION_MAJOR__ADD____LB__GNUTLS_VERSION_MINOR__GT__0__L_OR__GNUTLS_VERSION_PATCH__GEQ__20__RB____RB____GT__3)"), + + new TestCase("#if A && (B > C)", "A&&(B__GT__C)"), + new TestCase("#if (A && B) > C", "__LB__A__L_AND__B__RB____GT__C"), + new TestCase("#if C == (A || B)", "C__EQ____LB__A__L_OR__B__RB__"), + new TestCase("#if ((A && B) > C)", "(__LB__A__L_AND__B__RB____GT__C)"), + new TestCase("#if A && ((B + 1) > (C || D))", "A&&(__LB__B__ADD__1__RB____GT____LB__C__L_OR__D__RB__)"), + + new TestCase("#if __has_include", "HAS_INCLUDE_"), + new TestCase("#if defined __has_include", "DEFINED_HAS_INCLUDE_"), + new TestCase("#if __has_include()", "HAS_INCLUDE___LB____LT__nss3__DIV__nss__DOT__h__GT____RB__"), + new TestCase("#if __has_include()", "HAS_INCLUDE___LB____LT__nss__DOT__h__GT____RB__"), + new TestCase("#if __has_include(\"nss3/nss.h\")", "HAS_INCLUDE___LB____QUOTE__nss3__DIV__nss__DOT__h__QUOTE____RB__"), + new TestCase("#if __has_include(\"nss.h\")", "HAS_INCLUDE___LB____QUOTE__nss__DOT__h__QUOTE____RB__"), + + new TestCase("#if __has_attribute", "HAS_ATTRIBUTE_"), + new TestCase("#if defined __has_attribute", "DEFINED_HAS_ATTRIBUTE_"), + new TestCase("# if __has_attribute (nonnull)", "HAS_ATTRIBUTE___LB__nonnull__RB__"), + new TestCase("#if defined __has_attribute && __has_attribute (nonnull)", "DEFINED_HAS_ATTRIBUTE_&&HAS_ATTRIBUTE___LB__nonnull__RB__"), + + new TestCase("#if __has_cpp_attribute", "HAS_CPP_ATTRIBUTE_"), + new TestCase("#if defined __has_cpp_attribute", "DEFINED_HAS_CPP_ATTRIBUTE_"), + new TestCase("#if __has_cpp_attribute (nonnull)", "HAS_CPP_ATTRIBUTE___LB__nonnull__RB__"), + new TestCase("#if __has_cpp_attribute (nonnull) && A", "HAS_CPP_ATTRIBUTE___LB__nonnull__RB__&&A"), + + new TestCase("#if defined __has_c_attribute", "DEFINED_HAS_C_ATTRIBUTE_"), + new TestCase("#if __has_c_attribute", "HAS_C_ATTRIBUTE_"), + new TestCase("#if __has_c_attribute (nonnull)", "HAS_C_ATTRIBUTE___LB__nonnull__RB__"), + new TestCase("#if __has_c_attribute (nonnull) && A", "HAS_C_ATTRIBUTE___LB__nonnull__RB__&&A"), + + new TestCase("#if defined __has_builtin", "DEFINED_HAS_BUILTIN_"), + new TestCase("#if __has_builtin", "HAS_BUILTIN_"), + new TestCase("#if __has_builtin (__nonnull)", "HAS_BUILTIN___LB____nonnull__RB__"), + new TestCase("#if __has_builtin (nonnull) && A", "HAS_BUILTIN___LB__nonnull__RB__&&A"), + + new TestCase("#if A // Comment && B", "A"), + new TestCase("#if A /* Comment */ && B", "A&&B"), + new TestCase("#if A && B /* Multiline Comment", "A&&B"), + + new TestCase("#if A == B", "A__EQ__B"), + new TestCase("#if A == 1", "A__EQ__1"), + + new TestCase("#if defined A", "DEFINED_A"), + new TestCase("#if defined(A)", "DEFINED___LB__A__RB__"), + new TestCase("#if defined (A)", "DEFINED___LB__A__RB__"), + new TestCase("#if defined ( A )", "DEFINED___LB__A__RB__"), + new TestCase("#if (defined A)", "(DEFINED_A)"), + 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"), + + new TestCase("#if __has_include_next()", "__HAS_INCLUDE_NEXT___LB____LT__some__SUB__header__DOT__h__GT____RB__"), + new TestCase("#if __is_target_arch(x86)", "__IS_TARGET_ARCH___LB__x86__RB__"), + new TestCase("#if A || (defined(NAME) && (NAME >= 199630))", "A||(DEFINED___LB__NAME__RB__&&(NAME__GEQ__199630))"), + new TestCase("#if MACRO(part:part)", "MACRO___LB__part__COLON__part__RB__"), + new TestCase("#if MACRO(x=1)", "MACRO___LB__x__ASSIGN__1__RB__"), + new TestCase("#if A = 3", "A__ASSIGN__3"), + new TestCase("#if ' ' == 32", "__SQUOTE_____SQUOTE____EQ__32"), + new TestCase("#if (NAME<<1) > (1<= 199905) && (NAME < 1991011)) || (NAME >= 300000) || defined(NAME)", "(DEFINED___LB__NAME__RB__&&(NAME__GEQ__199905)&&(NAME__LT__1991011))||(NAME__GEQ__300000)||DEFINED___LB__NAME__RB__"), + new TestCase("#if __has_warning(\"-Wa-warning\"_foo)", + "__HAS_WARNING___LB____QUOTE____SUB__Wa__SUB__warning__QUOTE_____foo__RB__") ); } 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 */") + // 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 */") ); } 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__)") + 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__)") ); } @@ -137,8 +140,8 @@ private static List wontfixTestCases() { @MethodSource("testCases") public void testCase(TestCase testCase) throws UnparseableFormulaException { assertEquals( - testCase.expected, - new CPPDiffLineFormulaExtractor().extractFormula(testCase.formula()) + testCase.expected, + new CPPDiffLineFormulaExtractor().extractFormula(testCase.formula()) ); } @@ -146,7 +149,7 @@ public void testCase(TestCase testCase) throws UnparseableFormulaException { @MethodSource("throwingTestCases") public void throwingTestCase(ThrowingTestCase testCase) { assertThrows(UnparseableFormulaException.class, () -> - new CPPDiffLineFormulaExtractor().extractFormula(testCase.formula) + new CPPDiffLineFormulaExtractor().extractFormula(testCase.formula) ); } @@ -155,8 +158,8 @@ public void throwingTestCase(ThrowingTestCase testCase) { @MethodSource("wontfixTestCases") public void wontfixTestCase(TestCase testCase) throws UnparseableFormulaException { assertEquals( - testCase.expected, - new CPPDiffLineFormulaExtractor().extractFormula(testCase.formula()) + testCase.expected, + new CPPDiffLineFormulaExtractor().extractFormula(testCase.formula()) ); } diff --git a/src/test/java/JPPParserTest.java b/src/test/java/JPPParserTest.java new file mode 100644 index 000000000..2938989a8 --- /dev/null +++ b/src/test/java/JPPParserTest.java @@ -0,0 +1,149 @@ +import org.apache.commons.io.IOUtils; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.variantsync.diffdetective.diff.result.DiffParseException; +import org.variantsync.diffdetective.error.UnparseableFormulaException; +import org.variantsync.diffdetective.feature.PreprocessorAnnotationParser; +import org.variantsync.diffdetective.feature.jpp.JPPDiffLineFormulaExtractor; +import org.variantsync.diffdetective.util.IO; +import org.variantsync.diffdetective.variation.DiffLinesLabel; +import org.variantsync.diffdetective.variation.diff.VariationDiff; +import org.variantsync.diffdetective.variation.diff.parse.VariationDiffParseOptions; +import org.variantsync.diffdetective.variation.diff.parse.VariationDiffParser; +import org.variantsync.diffdetective.variation.diff.serialize.Format; +import org.variantsync.diffdetective.variation.diff.serialize.LineGraphExporter; +import org.variantsync.diffdetective.variation.diff.serialize.edgeformat.ChildOrderEdgeFormat; +import org.variantsync.diffdetective.variation.diff.serialize.nodeformat.FullNodeFormat; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.variantsync.diffdetective.util.Assert.fail; + +// Test cases for a parser of https://www.slashdev.ca/javapp/ +public class JPPParserTest { + private record TestCase(Input input, Expected expected) { + } + + private record ThrowingTestCase(String formula) { + } + + private static List> abstractionTests() { + return List.of( + /// #if expression + // expression := | [!]defined(name) + // expression := operand == operand + new JPPParserTest.TestCase<>("//#if 1 == -42", "1__EQ____U_MINUS__42"), + // expression := operand != operand + new JPPParserTest.TestCase<>("// #if 1 != 0", "1__NEQ__0"), + // expression := operand <= operand + new JPPParserTest.TestCase<>("//#if -1 <= 0", "__U_MINUS__1__LEQ__0"), + // expression := operand < operand + new JPPParserTest.TestCase<>("//#if \"str\" < 0", "__QUOTE__str__QUOTE____LT__0"), + // expression := operand >= operand + new JPPParserTest.TestCase<>("// #if \"str\" >= \"str\"", "__QUOTE__str__QUOTE____GEQ____QUOTE__str__QUOTE__"), + // expression := operand > operand + new JPPParserTest.TestCase<>("// #if 1.2 > 0", "1__DOT__2__GT__0"), + // expression := defined(name) + new JPPParserTest.TestCase<>("//#if defined(property)", "DEFINED_property"), + // expression := !defined(name) + new JPPParserTest.TestCase<>("//#if !defined(property)", "__U_NOT__DEFINED_property"), + // operand := ${property} + new JPPParserTest.TestCase<>("//#if ${os_version} == 4.1", "os_version__EQ__4__DOT__1"), + + /// #if expression and expression + new JPPParserTest.TestCase<>("//#if 1 > 2 and defined( FEAT_A )", "1__GT__2&&DEFINED_FEAT_A"), + + /// #if expression or expression + new JPPParserTest.TestCase<>("//#if !defined(left) or defined(right)", "__U_NOT__DEFINED_left||DEFINED_right"), + + /// #if expression and expression or expression + new JPPParserTest.TestCase<>("//#if ${os_version} == 4.1 and 1 > -42 or defined(ALL)", "os_version__EQ__4__DOT__1&&1__GT____U_MINUS__42||DEFINED_ALL") + ); + } + + private static List throwingTestCases() { + return List.of( + // Invalid macro + new JPPParserTest.ThrowingTestCase(""), + new JPPParserTest.ThrowingTestCase("#"), + new JPPParserTest.ThrowingTestCase("ifdef A"), + new JPPParserTest.ThrowingTestCase("#error A"), + new JPPParserTest.ThrowingTestCase("#iferror A"), + + // Empty formula + new JPPParserTest.ThrowingTestCase("//#if"), + new JPPParserTest.ThrowingTestCase("#if defined()"), + new JPPParserTest.ThrowingTestCase("#if ${} > 0"), + + // incomplete expressions + new JPPParserTest.ThrowingTestCase("#if 1 >"), + new JPPParserTest.ThrowingTestCase("#if == 2"), + new JPPParserTest.ThrowingTestCase("#if ${version} > ") + ); + } + + private static List> fullDiffTests() { + final Path basePath = Path.of("src", "test", "resources", "diffs", "jpp"); + return List.of( + new JPPParserTest.TestCase<>(basePath.resolve("basic_jpp.diff"), basePath.resolve("basic_jpp_expected.lg")) + ); + } + + @ParameterizedTest + @MethodSource("abstractionTests") + public void testCase(JPPParserTest.TestCase testCase) throws UnparseableFormulaException { + assertEquals( + testCase.expected, + new JPPDiffLineFormulaExtractor().extractFormula(testCase.input()) + ); + } + + @ParameterizedTest + @MethodSource("throwingTestCases") + public void throwingTestCase(JPPParserTest.ThrowingTestCase testCase) { + assertThrows(UnparseableFormulaException.class, () -> + new JPPDiffLineFormulaExtractor().extractFormula(testCase.formula) + ); + } + + @ParameterizedTest + @MethodSource("fullDiffTests") + public void fullDiffTestCase(JPPParserTest.TestCase testCase) throws IOException, DiffParseException { + VariationDiff variationDiff; + try (var inputFile = Files.newBufferedReader(testCase.input)) { + variationDiff = VariationDiffParser.createVariationDiff( + inputFile, + new VariationDiffParseOptions( + false, + false + ).withAnnotationParser(PreprocessorAnnotationParser.JPPAnnotationParser) + ); + } + + Path actualPath = testCase.input.getParent().resolve(testCase.input.getFileName() + "_actual"); + try (var output = IO.newBufferedOutputStream(actualPath)) { + new LineGraphExporter<>(new Format<>(new FullNodeFormat(), new ChildOrderEdgeFormat<>())) + .exportVariationDiff(variationDiff, output); + } + + try ( + var expectedFile = Files.newBufferedReader(testCase.expected); + var actualFile = Files.newBufferedReader(actualPath); + ) { + if (IOUtils.contentEqualsIgnoreEOL(expectedFile, actualFile)) { + // Delete output files if the test succeeded + Files.delete(actualPath); + } else { + // Keep output files if the test failed + fail("The VariationDiff in file " + testCase.input + " didn't parse correctly. " + + "Expected the content of " + testCase.expected + " but got the content of " + actualPath + ". "); + } + } + } + +} diff --git a/src/test/java/PropositionalFormulaParserTest.java b/src/test/java/PropositionalFormulaParserTest.java new file mode 100644 index 000000000..ee891f811 --- /dev/null +++ b/src/test/java/PropositionalFormulaParserTest.java @@ -0,0 +1,83 @@ +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.prop4j.And; +import org.prop4j.Literal; +import org.prop4j.Node; +import org.prop4j.Or; +import org.variantsync.diffdetective.feature.PropositionalFormulaParser; + +import java.util.List; + +import static org.variantsync.diffdetective.util.Assert.assertEquals; + +/** + * Class containing tests of the parsing behaviour for the default implementation of PropositionalFormulaParser. + * Goal: Special characters that occur in the output of a DiffLineFormulaExtractor must not confuse the parsing process of the PropositionalFormulaParser. + * It is not designed to extensively test the functionality of the PropositionalFormulaParser itself as this is expected to be done by FeatureIDE already. + * + * @author Maximilian Glumann + */ +public class PropositionalFormulaParserTest { + private record TestCase(String formula, Node expected) { + } + + /** + * These test cases are based on a subset of the CPPParserTest test cases. + * It is not necessary to keep all test cases from CPPParserTest as most of them result in a single but long Literal anyway. + */ + private static List testCases() { + return List.of( + new TestCase("A", new Literal("A")), + new TestCase("!(A)", new Literal("A", false)), + new TestCase("!A", new Literal("A", false)), + + new TestCase("A&&B", new And(new Literal("A"), new Literal("B"))), + new TestCase("A||B", new Or(new Literal("A"), new Literal("B"))), + new TestCase("A&&(B||C)", new And(new Literal("A"), new Or(new Literal("B"), new Literal("C")))), + new TestCase("A&&B||C", new Or(new And(new Literal("A"), new Literal("B")), new Literal("C"))), + + new TestCase("A__GEQ__B&&C__GT__D", new And(new Literal("A__GEQ__B"), new Literal("C__GT__D"))), + new TestCase("DEFINED___LB__A__RB__&&__LB__B__MUL__2__RB____GT__C", new And(new Literal("DEFINED___LB__A__RB__"), new Literal("__LB__B__MUL__2__RB____GT__C"))), + new TestCase("(STDC__EQ__1)&&(DEFINED___LB__LARGE__RB__||DEFINED___LB__COMPACT__RB__)", new And(new Literal("STDC__EQ__1"), new Or(new Literal("DEFINED___LB__LARGE__RB__"), new Literal("DEFINED___LB__COMPACT__RB__")))), + new TestCase("APR_CHARSET_EBCDIC&&!(__LB____SQUOTE__Z__SQUOTE____SUB____SQUOTE__A__SQUOTE____RB____EQ__25)", new And(new Literal("APR_CHARSET_EBCDIC"), new Literal("__LB____SQUOTE__Z__SQUOTE____SUB____SQUOTE__A__SQUOTE____RB____EQ__25", false))), + new TestCase("A&&(B__GT__C)", new And(new Literal("A"), new Literal("B__GT__C"))), + new TestCase("A&&(__LB__B__ADD__1__RB____GT____LB__C__L_OR__D__RB__)", new And(new Literal("A"), new Literal("__LB__B__ADD__1__RB____GT____LB__C__L_OR__D__RB__"))), + new TestCase("DEFINED_HAS_ATTRIBUTE_&&HAS_ATTRIBUTE___LB__nonnull__RB__", new And(new Literal("DEFINED_HAS_ATTRIBUTE_"), new Literal("HAS_ATTRIBUTE___LB__nonnull__RB__"))), + new TestCase("HAS_BUILTIN___LB__nonnull__RB__&&A", new And(new Literal("HAS_BUILTIN___LB__nonnull__RB__"), new Literal("A"))), + new TestCase("A||(DEFINED___LB__NAME__RB__&&(NAME__GEQ__199630))", new Or(new Literal("A"), new And(new Literal("DEFINED___LB__NAME__RB__"), new Literal("NAME__GEQ__199630")))), + new TestCase("(DEFINED___LB__NAME__RB__&&(NAME__GEQ__199905)&&(NAME__LT__1991011))||(NAME__GEQ__300000)||DEFINED___LB__NAME__RB__", new Or(new And(new Literal("DEFINED___LB__NAME__RB__"), new And(new Literal("NAME__GEQ__199905"), new Literal("NAME__LT__1991011"))), new Or(new Literal("NAME__GEQ__300000"), new Literal("DEFINED___LB__NAME__RB__")))), + new TestCase("1__GT____U_MINUS__42", new Literal("1__GT____U_MINUS__42")), + new TestCase("1__GT____U_PLUS__42", new Literal("1__GT____U_PLUS__42")), + new TestCase("42__GT____U_TILDE__A", new Literal("42__GT____U_TILDE__A")), + new TestCase("A__ADD__B__GT__42", new Literal("A__ADD__B__GT__42")), + new TestCase("A__LSHIFT__B", new Literal("A__LSHIFT__B")), + new TestCase("A__THEN__B__COLON__C", new Literal("A__THEN__B__COLON__C")), + new TestCase("A__MUL____LB__B__ADD__C__RB__", new Literal("A__MUL____LB__B__ADD__C__RB__")), + new TestCase("(__LB____SQUOTE__Z__SQUOTE____SUB____SQUOTE__A__SQUOTE____RB____EQ__25)", new Literal("__LB____SQUOTE__Z__SQUOTE____SUB____SQUOTE__A__SQUOTE____RB____EQ__25")), + new TestCase("(__LB__GNUTLS_VERSION_MAJOR__ADD____LB__GNUTLS_VERSION_MINOR__GT__0__L_OR__GNUTLS_VERSION_PATCH__GEQ__20__RB____RB____GT__3)", new Literal("__LB__GNUTLS_VERSION_MAJOR__ADD____LB__GNUTLS_VERSION_MINOR__GT__0__L_OR__GNUTLS_VERSION_PATCH__GEQ__20__RB____RB____GT__3")), + new TestCase("(__LB__A__L_AND__B__RB____GT__C)", new Literal("__LB__A__L_AND__B__RB____GT__C")), + new TestCase("A__EQ__B", new Literal("A__EQ__B")), + new TestCase("(DEFINED_A)", new Literal("DEFINED_A")), + new TestCase("MACRO___LB__A__B__RB____EQ__1", new Literal("MACRO___LB__A__B__RB____EQ__1")), + new TestCase("ifndef", new Literal("ifndef")), + new TestCase("__HAS_WARNING___LB____QUOTE____SUB__Wa__SUB__warning__QUOTE_____foo__RB__", new Literal("__HAS_WARNING___LB____QUOTE____SUB__Wa__SUB__warning__QUOTE_____foo__RB__")) + ); + } + + /** + * Each test case compares the output of the default PropositionalFormularParser to the expected output. + * This comparison is performed using the equivalence defined by org.prop4j.Node from FeatureIDE. + * Therefore, nodes describing equivalent propositional formulas in different tree structures are not considered equal. + * As long as FeatureIDE produces a deterministic and consistent tree structure in its output, these tests will succeed. + * Because DiffDetective desires not only a correct but also a deterministic and consistent parser output, + * it is intended that these tests also break, if FeatureIDE changes its parsing behaviour in the future. + */ + @ParameterizedTest + @MethodSource("testCases") + public void testCase(TestCase testCase) { + assertEquals( + testCase.expected, + PropositionalFormulaParser.Default.parse(testCase.formula) + ); + } +} \ No newline at end of file diff --git a/src/test/java/TestMultiLineMacros.java b/src/test/java/TestMultiLineMacros.java index 2ebc48dfe..41004517c 100644 --- a/src/test/java/TestMultiLineMacros.java +++ b/src/test/java/TestMultiLineMacros.java @@ -1,30 +1,9 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; -import org.junit.jupiter.params.provider.ValueSource; -import org.tinylog.Logger; -import org.variantsync.diffdetective.variation.DiffLinesLabel; -import org.variantsync.diffdetective.variation.diff.VariationDiff; -import org.variantsync.diffdetective.feature.CPPAnnotationParser; -import org.variantsync.diffdetective.variation.diff.parse.VariationDiffParseOptions; -import org.variantsync.diffdetective.variation.diff.parse.VariationDiffParser; -import org.variantsync.diffdetective.variation.diff.serialize.LineGraphExportOptions; -import org.variantsync.diffdetective.variation.diff.serialize.VariationDiffSerializeDebugData; -import org.variantsync.diffdetective.variation.diff.serialize.GraphFormat; -import org.variantsync.diffdetective.variation.diff.serialize.LineGraphExport; -import org.variantsync.diffdetective.variation.diff.serialize.edgeformat.DefaultEdgeLabelFormat; -import org.variantsync.diffdetective.variation.diff.serialize.nodeformat.DebugDiffNodeFormat; -import org.variantsync.diffdetective.variation.diff.serialize.treeformat.CommitDiffVariationDiffLabelFormat; import org.variantsync.diffdetective.diff.result.DiffParseException; -import org.variantsync.diffdetective.util.IO; -import org.variantsync.diffdetective.util.StringUtils; -import static org.junit.jupiter.api.Assertions.assertNotNull; - -import java.io.BufferedReader; import java.io.IOException; -import java.nio.file.Files; import java.nio.file.Path; -import java.util.ArrayList; import java.util.stream.Stream; public class TestMultiLineMacros { diff --git a/src/test/java/TreeDiffingTest.java b/src/test/java/TreeDiffingTest.java index b494dbf9c..1bcf7c246 100644 --- a/src/test/java/TreeDiffingTest.java +++ b/src/test/java/TreeDiffingTest.java @@ -1,15 +1,14 @@ import com.github.gumtreediff.matchers.Matcher; import com.github.gumtreediff.matchers.Matchers; - import org.apache.commons.io.IOUtils; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; import org.variantsync.diffdetective.diff.result.DiffParseException; -import org.variantsync.diffdetective.feature.CPPAnnotationParser; +import org.variantsync.diffdetective.feature.PreprocessorAnnotationParser; import org.variantsync.diffdetective.util.IO; import org.variantsync.diffdetective.variation.DiffLinesLabel; -import org.variantsync.diffdetective.variation.diff.construction.GumTreeDiff; import org.variantsync.diffdetective.variation.diff.VariationDiff; +import org.variantsync.diffdetective.variation.diff.construction.GumTreeDiff; import org.variantsync.diffdetective.variation.diff.parse.VariationDiffParseOptions; import org.variantsync.diffdetective.variation.diff.parse.VariationDiffParser; import org.variantsync.diffdetective.variation.diff.serialize.Format; @@ -21,19 +20,19 @@ import org.variantsync.diffdetective.variation.tree.VariationTree; import org.variantsync.diffdetective.variation.tree.source.LocalFileSource; -import static org.junit.jupiter.api.Assertions.fail; - import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.util.regex.Pattern; import java.util.stream.Stream; +import static org.junit.jupiter.api.Assertions.fail; import static org.variantsync.diffdetective.variation.diff.Time.BEFORE; public class TreeDiffingTest { private final static Path testDir = Constants.RESOURCE_DIR.resolve("tree-diffing"); private static Pattern expectedFileNameRegex = Pattern.compile("([^_]+)_([^_]+)_expected.lg"); + private static record TestCase(String basename, String matcherName, Matcher matcher) { public Path beforeEdit() { return testDir.resolve(String.format("%s.before", basename())); @@ -58,20 +57,20 @@ public Path visualisation() { private static Stream testCases() throws IOException { return Files - .list(testDir) - .mapMulti(((path, result) -> { - String filename = path.getFileName().toString(); - var filenameMatcher = expectedFileNameRegex.matcher(filename); - if (filenameMatcher.matches()) { - var treeMatcherName = filenameMatcher.group(2); - - result.accept(new TestCase( - filenameMatcher.group(1), - treeMatcherName, - Matchers.getInstance().getMatcher(treeMatcherName)) - ); - } - })); + .list(testDir) + .mapMulti(((path, result) -> { + String filename = path.getFileName().toString(); + var filenameMatcher = expectedFileNameRegex.matcher(filename); + if (filenameMatcher.matches()) { + var treeMatcherName = filenameMatcher.group(2); + + result.accept(new TestCase( + filenameMatcher.group(1), + treeMatcherName, + Matchers.getInstance().getMatcher(treeMatcherName)) + ); + } + })); } @ParameterizedTest @@ -84,7 +83,7 @@ public void testCase(TestCase testCase) throws IOException, DiffParseException { try (var output = IO.newBufferedOutputStream(testCase.actual())) { new LineGraphExporter<>(new Format<>(new FullNodeFormat(), new ChildOrderEdgeFormat<>())) - .exportVariationDiff(variationDiff, output); + .exportVariationDiff(variationDiff, output); } try ( @@ -97,16 +96,16 @@ public void testCase(TestCase testCase) throws IOException, DiffParseException { } else { // Keep output files if the test failed new TikzExporter<>(new Format<>(new FullNodeFormat(), new DefaultEdgeLabelFormat<>())) - .exportFullLatexExample(variationDiff, testCase.visualisation()); + .exportFullLatexExample(variationDiff, testCase.visualisation()); fail(String.format( - "The diff of %s and %s is not as expected. " + - "Expected the content of %s but got the content of %s. " + - "Note: A visualisation is available at %s", - testCase.beforeEdit(), - testCase.afterEdit(), - testCase.expected(), - testCase.actual(), - testCase.visualisation() + "The diff of %s and %s is not as expected. " + + "Expected the content of %s but got the content of %s. " + + "Note: A visualisation is available at %s", + testCase.beforeEdit(), + testCase.afterEdit(), + testCase.expected(), + testCase.actual(), + testCase.visualisation() )); } } @@ -115,14 +114,14 @@ public void testCase(TestCase testCase) throws IOException, DiffParseException { public VariationTree parseVariationTree(Path filename) throws IOException, DiffParseException { try (var file = Files.newBufferedReader(filename)) { return new VariationTree<>( - VariationDiffParser.createVariationTree( - file, - new VariationDiffParseOptions( - CPPAnnotationParser.Default, - false, - false) - ).getRoot().projection(BEFORE).toVariationTree(), - new LocalFileSource(filename) + VariationDiffParser.createVariationTree( + file, + new VariationDiffParseOptions( + PreprocessorAnnotationParser.CPPAnnotationParser, + false, + false) + ).getRoot().projection(BEFORE).toVariationTree(), + new LocalFileSource(filename) ); } } diff --git a/src/test/resources/diffs/jpp/.gitignore b/src/test/resources/diffs/jpp/.gitignore new file mode 100644 index 000000000..6aa79ba57 --- /dev/null +++ b/src/test/resources/diffs/jpp/.gitignore @@ -0,0 +1,2 @@ +*_actual.lg +/tex/ diff --git a/src/test/resources/diffs/jpp/basic_jpp.diff b/src/test/resources/diffs/jpp/basic_jpp.diff new file mode 100644 index 000000000..69356babb --- /dev/null +++ b/src/test/resources/diffs/jpp/basic_jpp.diff @@ -0,0 +1,20 @@ ++package org.argouml.application; ++ ++import javax.swing.UIManager; ++//#if defined(LOGGING) ++import org.apache.log4j.BasicConfigurator; ++import org.apache.log4j.Level; ++import org.apache.log4j.Logger; ++//#endif ++import org.argouml.application.api.CommandLineInterface; ++import org.argouml.application.security.ArgoAwtExceptionHandler; ++//#if defined(COGNITIVE) ++//@#$LPS-COGNITIVE:GranularityType:Import ++import org.argouml.cognitive.AbstractCognitiveTranslator; ++import org.argouml.cognitive.ui.ToDoPane; ++//#endif ++import org.argouml.ui.cmd.InitUiCmdSubsystem; ++import org.argouml.ui.cmd.PrintManager; ++//#if defined(COGNITIVE) and defined(DEPLOYMENTDIAGRAM) ++import org.argouml.uml.diagram.activity.ui.InitActivityDiagram; ++//#endif \ No newline at end of file diff --git a/src/test/resources/diffs/jpp/basic_jpp_expected.lg b/src/test/resources/diffs/jpp/basic_jpp_expected.lg new file mode 100644 index 000000000..c47721aa1 --- /dev/null +++ b/src/test/resources/diffs/jpp/basic_jpp_expected.lg @@ -0,0 +1,35 @@ +v 16 NON;IF;(old: -1, diff: -1, new: -1);(old: -1, diff: -1, new: -1);True +v 131 ADD;ARTIFACT;(old: -1, diff: 1, new: 1);(old: -1, diff: 2, new: 2);;package org.argouml.application; +v 195 ADD;ARTIFACT;(old: -1, diff: 2, new: 2);(old: -1, diff: 3, new: 3);; +v 259 ADD;ARTIFACT;(old: -1, diff: 3, new: 3);(old: -1, diff: 4, new: 4);;import javax.swing.UIManager; +v 320 ADD;IF;(old: -1, diff: 4, new: 4);(old: -1, diff: 8, new: 8);DEFINED_LOGGING;//#if defined(LOGGING) +v 387 ADD;ARTIFACT;(old: -1, diff: 5, new: 5);(old: -1, diff: 6, new: 6);;import org.apache.log4j.BasicConfigurator; +v 451 ADD;ARTIFACT;(old: -1, diff: 6, new: 6);(old: -1, diff: 7, new: 7);;import org.apache.log4j.Level; +v 515 ADD;ARTIFACT;(old: -1, diff: 7, new: 7);(old: -1, diff: 8, new: 8);;import org.apache.log4j.Logger; +v 643 ADD;ARTIFACT;(old: -1, diff: 9, new: 9);(old: -1, diff: 10, new: 10);;import org.argouml.application.api.CommandLineInterface; +v 707 ADD;ARTIFACT;(old: -1, diff: 10, new: 10);(old: -1, diff: 11, new: 11);;import org.argouml.application.security.ArgoAwtExceptionHandler; +v 768 ADD;IF;(old: -1, diff: 11, new: 11);(old: -1, diff: 15, new: 15);DEFINED_COGNITIVE;//#if defined(COGNITIVE) +v 835 ADD;ARTIFACT;(old: -1, diff: 12, new: 12);(old: -1, diff: 13, new: 13);;//@#$LPS-COGNITIVE:GranularityType:Import +v 899 ADD;ARTIFACT;(old: -1, diff: 13, new: 13);(old: -1, diff: 14, new: 14);;import org.argouml.cognitive.AbstractCognitiveTranslator; +v 963 ADD;ARTIFACT;(old: -1, diff: 14, new: 14);(old: -1, diff: 15, new: 15);;import org.argouml.cognitive.ui.ToDoPane; +v 1091 ADD;ARTIFACT;(old: -1, diff: 16, new: 16);(old: -1, diff: 17, new: 17);;import org.argouml.ui.cmd.InitUiCmdSubsystem; +v 1155 ADD;ARTIFACT;(old: -1, diff: 17, new: 17);(old: -1, diff: 18, new: 18);;import org.argouml.ui.cmd.PrintManager; +v 1216 ADD;IF;(old: -1, diff: 18, new: 18);(old: -1, diff: 20, new: 20);DEFINED_COGNITIVE & DEFINED_DEPLOYMENTDIAGRAM;//#if defined(COGNITIVE) and defined(DEPLOYMENTDIAGRAM) +v 1283 ADD;ARTIFACT;(old: -1, diff: 19, new: 19);(old: -1, diff: 20, new: 20);;import org.argouml.uml.diagram.activity.ui.InitActivityDiagram; +e 131 16 a;-1,0 +e 195 16 a;-1,1 +e 259 16 a;-1,2 +e 320 16 a;-1,3 +e 387 320 a;-1,0 +e 451 320 a;-1,1 +e 515 320 a;-1,2 +e 643 16 a;-1,4 +e 707 16 a;-1,5 +e 768 16 a;-1,6 +e 835 768 a;-1,0 +e 899 768 a;-1,1 +e 963 768 a;-1,2 +e 1091 16 a;-1,7 +e 1155 16 a;-1,8 +e 1216 16 a;-1,9 +e 1283 1216 a;-1,0