diff --git a/.github/workflows/jreleaser.yml b/.github/workflows/jreleaser.yml index a7d1e1475b7..b4cbc0566de 100644 --- a/.github/workflows/jreleaser.yml +++ b/.github/workflows/jreleaser.yml @@ -34,7 +34,7 @@ jobs: JRELEASER_NEXUS2_MAVEN_CENTRAL_PASSWORD: ${{ secrets.JRELEASER_NEXUS2_MAVEN_CENTRAL_PASSWORD }} - name: Sign artifacts with sigstore/cosign - uses: actions/attest-build-provenance@1c608d11d69870c2092266b3f9a6f3abbf17002c # v1.4.3 + uses: actions/attest-build-provenance@ef244123eb79f2f7a7e75d99086184180e6d0018 # v1.4.4 with: subject-path: './target/staging-deploy/**/*.jar' diff --git a/.github/workflows/qodana.yml b/.github/workflows/qodana.yml index 221313dc38a..1f7ad9f66e2 100644 --- a/.github/workflows/qodana.yml +++ b/.github/workflows/qodana.yml @@ -22,7 +22,7 @@ jobs: with: args: --source-directory,./src/main/java , --fail-threshold, 0 post-pr-comment: "false" - - uses: github/codeql-action/upload-sarif@662472033e021d55d94146f66f6058822b0b39fd # v3 + - uses: github/codeql-action/upload-sarif@396bb3e45325a47dd9ef434068033c6d5bb0d11a # v3 with: sarif_file: ${{ runner.temp }}/qodana/results/qodana.sarif.json code-quality-spoon-javadoc: @@ -37,7 +37,7 @@ jobs: with: args: --source-directory,./spoon-javadoc/src/main/java , --fail-threshold, 0 post-pr-comment: "false" - - uses: github/codeql-action/upload-sarif@662472033e021d55d94146f66f6058822b0b39fd # v3 + - uses: github/codeql-action/upload-sarif@396bb3e45325a47dd9ef434068033c6d5bb0d11a # v3 with: sarif_file: ${{ runner.temp }}/qodana/results/qodana.sarif.json code-quality-spoon-control-flow: @@ -52,6 +52,6 @@ jobs: with: args: --source-directory,./spoon-control-flow/src/main/java , --fail-threshold, 0 post-pr-comment: "false" - - uses: github/codeql-action/upload-sarif@662472033e021d55d94146f66f6058822b0b39fd # v3 + - uses: github/codeql-action/upload-sarif@396bb3e45325a47dd9ef434068033c6d5bb0d11a # v3 with: sarif_file: ${{ runner.temp }}/qodana/results/qodana.sarif.json diff --git a/.github/workflows/scorecards.yml b/.github/workflows/scorecards.yml index f5107f36fc6..c0d5a8e6ba0 100644 --- a/.github/workflows/scorecards.yml +++ b/.github/workflows/scorecards.yml @@ -71,6 +71,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard. - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@662472033e021d55d94146f66f6058822b0b39fd # v3.27.0 + uses: github/codeql-action/upload-sarif@396bb3e45325a47dd9ef434068033c6d5bb0d11a # v3.27.3 with: sarif_file: results.sarif diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index a0746640dfc..ea632e5e3c9 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -120,7 +120,7 @@ jobs: - name: Time nix setup run: nix develop ${{ env.NIX_OPTIONS }} .#extraChecks --command true - name: Build spoon - run: nix develop ${{ env.NIX_OPTIONS }} .#extraChecks --command mvn -f spoon-pom -B install -Dmaven.test.skip=true -DskipDepClean + run: nix develop ${{ env.NIX_OPTIONS }} .#extraChecks --command mvn -f spoon-pom -B install -Dmaven.test.skip=true - name: Run Javadoc quality check run: nix develop ${{ env.NIX_OPTIONS }} .#extraChecks --command javadoc-quality diff --git a/chore/check-reproducible-builds.sh b/chore/check-reproducible-builds.sh index 5da16c50ca7..74fc34b7060 100755 --- a/chore/check-reproducible-builds.sh +++ b/chore/check-reproducible-builds.sh @@ -4,7 +4,7 @@ set -e build() { - mvn -f spoon-pom clean package -DskipDepClean -DskipTests -Dmaven.javadoc.skip > /dev/null + mvn -f spoon-pom clean package -DskipTests -Dmaven.javadoc.skip > /dev/null } compare_files() { diff --git a/doc/README.md b/doc/README.md index f2a80f70321..87924116d54 100644 --- a/doc/README.md +++ b/doc/README.md @@ -1,4 +1,9 @@ -This directory contains the source code of the Spoon website +## Documentation for Spoon + +* CI/CD, see +* Supply-chain, see + +### Deploy the Website To deploy an instance of this website, we use a personal script because the structure of this project isn't standard. We can't have markdown files outside the working directory of Jekyll. So: diff --git a/doc/SUPPLY-CHAIN.md b/doc/SUPPLY-CHAIN.md new file mode 100644 index 00000000000..44f5b92bee1 --- /dev/null +++ b/doc/SUPPLY-CHAIN.md @@ -0,0 +1,60 @@ +# Supply chain +## Attest build artifacts +The Spoon CI/CD pipeline attests all released artifacts by publishing attestations to the [sigstore/rekor](https://www.sigstore.dev/) public-good instance as well as storing them in the [Github's attestation registry](https://github.com/INRIA/spoon/attestations). Attestations are published using Github's [attest-build-provenance](https://github.com/actions/attest-build-provenance) action as a step in the [jreleaser job](https://github.com/ludvigch/spoon/blob/master/.github/workflows/jreleaser.yml). A list of the attestations created for a release can be found in the summary of a job and the sigstore/rekor links for each attestation can be found in the log of the jreleaser job. + +## Finding attestations + +Rekor is searchable with the hash of an attested artifact, for example attestation for spoon-core-11.1.1-beta-11-jar-with-dependencies.jar can be found at + + +Github provides an [`attestations` tab](https://github.com/INRIA/spoon/attestations) for all repos and a [REST API Endpoint](https://docs.github.com/en/rest/users/attestations) + +## Verifying attestations + +The most straight-forward approach is to use GitHub CLI's [`gh attestation verify`](https://cli.github.com/manual/gh_attestation_verify) to verify the attestation of an artifact by running: + +`gh attestation verify .jar -R INRIA/spoon` + +For example, let's verify the [spoon-core-11.1.1-beta-11-jar-with-dependencies.jar](https://repo1.maven.org/maven2/fr/inria/gforge/spoon/spoon-core/11.1.1-beta-11/spoon-core-11.1.1-beta-11-jar-with-dependencies.jar) artifact. + +### Alternative 1: Using GitHub API + +Install `gh`, see doc at + +``` +curl -O https://repo1.maven.org/maven2/fr/inria/gforge/spoon/spoon-core/11.1.1-beta-11/spoon-core-11.1.1-beta-11-jar-with-dependencies.jar +gh attestation verify spoon-core-11.1.1-beta-11-jar-with-dependencies.jar -R INRIA/spoon +``` + +Output: +``` +Loaded digest sha256:804c2ab449cc16052b467edc3ab1f7cf931f8e679685c0e16fab2fcc16ecfb41 for file://spoon-core-11.1.1-beta-11-jar-with-dependencies.jar +Loaded 1 attestation from GitHub API +✓ Verification succeeded! + +sha256:804c2ab449cc16052b467edc3ab1f7cf931f8e679685c0e16fab2fcc16ecfb41 was attested by: +REPO PREDICATE_TYPE WORKFLOW +INRIA/spoon https://slsa.dev/provenance/v1 .github/workflows/jreleaser.yml@refs/heads/master + +``` + +### Alternative 2: Using a downloaded attestation + +[Dowload the attestation.](https://github.com/INRIA/spoon/attestations/2750640/download) + +``` +curl -o ./INRIA-spoon-attestation-2750640.sigstore.json https://github.com/INRIA/spoon/attestations/2750640/download +gh attestation verify spoon-core-11.1.1-beta-11-jar-with-dependencies.jar -R INRIA/spoon --bundle ./INRIA-spoon-attestation-2750640.sigstore.json +``` + +Output: +``` +Loaded digest sha256:804c2ab449cc16052b467edc3ab1f7cf931f8e679685c0e16fab2fcc16ecfb41 for file://spoon-core-11.1.1-beta-11-jar-with-dependencies.jar +Loaded 1 attestation from INRIA-spoon-attestation-2750640.sigstore.json +✓ Verification succeeded! + +sha256:804c2ab449cc16052b467edc3ab1f7cf931f8e679685c0e16fab2fcc16ecfb41 was attested by: +REPO PREDICATE_TYPE WORKFLOW +INRIA/spoon https://slsa.dev/provenance/v1 .github/workflows/jreleaser.yml@refs/heads/master + +``` diff --git a/flake.lock b/flake.lock index b03cf135bd9..2f5f887cbbc 100644 --- a/flake.lock +++ b/flake.lock @@ -2,11 +2,11 @@ "nodes": { "nixpkgs": { "locked": { - "lastModified": 1729880355, - "narHash": "sha256-RP+OQ6koQQLX5nw0NmcDrzvGL8HDLnyXt/jHhL1jwjM=", + "lastModified": 1731139594, + "narHash": "sha256-IigrKK3vYRpUu+HEjPL/phrfh7Ox881er1UEsZvw9Q4=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "18536bf04cd71abd345f9579158841376fdd0c5a", + "rev": "76612b17c0ce71689921ca12d9ffdc9c23ce40b2", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index 2f79c59e4a3..91d82867bd7 100644 --- a/flake.nix +++ b/flake.nix @@ -115,8 +115,6 @@ mvn -q checkstyle:checkstyle -Pcheckstyle-test # Check documentation links python3 ./chore/check-links-in-doc.py - # Analyze dependencies through DepClean in spoon-core - # mvn -q depclean:depclean pushd spoon-decompiler || exit 1 mvn -q versions:use-latest-versions -DallowSnapshots=true -Dincludes=fr.inria.gforge.spoon @@ -124,7 +122,6 @@ git diff mvn -q test mvn -q checkstyle:checkstyle license:check - # mvn -q depclean:depclean popd || exit 1 pushd spoon-control-flow || exit 1 @@ -140,7 +137,6 @@ mvn -q versions:update-parent -DallowSnapshots=true git diff mvn -q test - # mvn -q depclean:depclean popd || exit 1 pushd spoon-smpl || exit 1 @@ -149,7 +145,6 @@ git diff mvn -q -Djava.src.version=17 test mvn -q checkstyle:checkstyle license:check - # mvn -q depclean:depclean popd || exit 1 ''); extraRemote = pkgs.writeScriptBin "extra-remote" '' diff --git a/pom.xml b/pom.xml index 288effc0b21..a7e79a98dc8 100644 --- a/pom.xml +++ b/pom.xml @@ -91,7 +91,7 @@ com.fasterxml.jackson.core jackson-databind - 2.18.0 + 2.18.1 diff --git a/spoon-control-flow/pom.xml b/spoon-control-flow/pom.xml index f981ef5865e..c6e9aff6f2c 100644 --- a/spoon-control-flow/pom.xml +++ b/spoon-control-flow/pom.xml @@ -84,12 +84,6 @@ 1.5.2 - - junit - junit - 4.13.2 - test - diff --git a/spoon-dataflow/gradle/wrapper/gradle-wrapper.properties b/spoon-dataflow/gradle/wrapper/gradle-wrapper.properties index df97d72b8b9..94113f200e6 100644 --- a/spoon-dataflow/gradle/wrapper/gradle-wrapper.properties +++ b/spoon-dataflow/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.11-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/spoon-pom/pom.xml b/spoon-pom/pom.xml index 0716da976ab..214ea89db6b 100644 --- a/spoon-pom/pom.xml +++ b/spoon-pom/pom.xml @@ -194,24 +194,6 @@ deploy - - - se.kth.castor - depclean-maven-plugin - 2.0.6 - - - - depclean - - - - true - true - - - - @@ -247,7 +229,7 @@ maven-javadoc-plugin - 3.10.1 + 3.11.1 maven-project-info-reports-plugin @@ -267,7 +249,7 @@ maven-surefire-plugin - 3.5.1 + 3.5.2 diff --git a/src/main/java/spoon/reflect/factory/CodeFactory.java b/src/main/java/spoon/reflect/factory/CodeFactory.java index 62fe24f068c..541278cc8f4 100644 --- a/src/main/java/spoon/reflect/factory/CodeFactory.java +++ b/src/main/java/spoon/reflect/factory/CodeFactory.java @@ -369,7 +369,13 @@ public CtCatchVariable createCatchVariable(CtTypeReference type, Strin * variable (strong referencing). */ public CtCatchVariableReference createCatchVariableReference(CtCatchVariable catchVariable) { - return factory.Core().createCatchVariableReference().setType(catchVariable.getType()).>setSimpleName(catchVariable.getSimpleName()); + CtCatchVariableReference ref = factory.Core().createCatchVariableReference(); + + ref.setType(catchVariable.getType() == null ? null : catchVariable.getType().clone()); + ref.setSimpleName(catchVariable.getSimpleName()); + ref.setParent(catchVariable); + + return ref; } /** @@ -429,7 +435,10 @@ public CtVariableAccess createVariableRead(CtVariableReference variabl va = factory.Core().createFieldRead(); // creates a this target for non-static fields to avoid name conflicts... if (!isStatic) { - ((CtFieldAccess) va).setTarget(createThisAccess(((CtFieldReference) variable).getDeclaringType())); + // We do not want to change the parent of the declaring type, so clone here + ((CtFieldAccess) va).setTarget( + createThisAccess(((CtFieldReference) variable).getDeclaringType().clone()) + ); } } else { va = factory.Core().createVariableRead(); diff --git a/src/main/java/spoon/reflect/visitor/CommentHelper.java b/src/main/java/spoon/reflect/visitor/CommentHelper.java index 4e6bbb4a104..0203e7c2d6e 100644 --- a/src/main/java/spoon/reflect/visitor/CommentHelper.java +++ b/src/main/java/spoon/reflect/visitor/CommentHelper.java @@ -98,7 +98,7 @@ static void printCommentContent(PrinterHelper printer, CtComment comment, Functi if (commentType == CtComment.CommentType.BLOCK) { printer.write(transfo.apply(line)); if (hasMoreThanOneElement(content.lines())) { - printer.write(CtComment.LINE_SEPARATOR); + printer.writeln(); } } else { printer.write(transfo.apply(line)).writeln(); // removing spaces at the end of the space diff --git a/src/main/java/spoon/reflect/visitor/ModelConsistencyChecker.java b/src/main/java/spoon/reflect/visitor/ModelConsistencyChecker.java index 7415990af0d..78d7bef10cf 100644 --- a/src/main/java/spoon/reflect/visitor/ModelConsistencyChecker.java +++ b/src/main/java/spoon/reflect/visitor/ModelConsistencyChecker.java @@ -10,10 +10,14 @@ import spoon.compiler.Environment; import spoon.reflect.declaration.CtElement; import spoon.reflect.declaration.CtNamedElement; +import spoon.support.Internal; import spoon.support.Level; import java.util.ArrayDeque; +import java.util.ArrayList; import java.util.Deque; +import java.util.List; +import java.util.stream.Collectors; /** * This scanner checks that a program model is consistent with regards to the @@ -30,11 +34,13 @@ public class ModelConsistencyChecker extends CtScanner { Deque stack = new ArrayDeque<>(); + private final List inconsistentElements = new ArrayList<>(); + /** * Creates a new model consistency checker. * * @param environment - * the environment where to report errors + * the environment where to report errors, if null, no errors are reported * @param fixInconsistencies * automatically fix the inconsistencies rather than reporting * warnings (to report warnings, set this to false) @@ -48,22 +54,33 @@ public ModelConsistencyChecker(Environment environment, boolean fixInconsistenci this.fixNullParents = fixNullParents; } + /** + * Lists the inconsistencies in the given element and its children. + * + * @param ctElement the element to check + * @return a list of inconsistencies + */ + @Internal + public static List listInconsistencies(CtElement ctElement) { + ModelConsistencyChecker checker = new ModelConsistencyChecker(null, false, false); + checker.scan(ctElement); + return checker.inconsistentElements(); + } + /** * Enters an element. */ @Override public void enter(CtElement element) { - if (!stack.isEmpty()) { - if (!element.isParentInitialized() || element.getParent() != stack.peek()) { - if ((!element.isParentInitialized() && fixNullParents) || (element.getParent() != stack.peek() && fixInconsistencies)) { - element.setParent(stack.peek()); - } else { - final String name = element instanceof CtNamedElement ? " - " + ((CtNamedElement) element).getSimpleName() : ""; - environment.report(null, Level.WARN, - (element.isParentInitialized() ? "inconsistent" : "null") + " parent for " + element.getClass() + name + " - " + element.getPosition() + " - " + stack.peek() - .getPosition()); - dumpStack(); - } + if (!stack.isEmpty() && (!element.isParentInitialized() || element.getParent() != stack.peek())) { + InconsistentElements inconsistentElements = new InconsistentElements(element, List.copyOf(stack)); + this.inconsistentElements.add(inconsistentElements); + + if ((!element.isParentInitialized() && fixNullParents) || (element.getParent() != stack.peek() && fixInconsistencies)) { + element.setParent(stack.peek()); + } else if (environment != null) { + environment.report(null, Level.WARN, inconsistentElements.reason()); + this.dumpStack(); } } stack.push(element); @@ -77,11 +94,91 @@ protected void exit(CtElement e) { stack.pop(); } + /** + * Gets the list of elements that are considered inconsistent. + *

+ * If {@link #fixInconsistencies} is set to true, this list will + * contain all the elements that have been fixed. + * + * @return the invalid elements + */ + private List inconsistentElements() { + return List.copyOf(inconsistentElements); + } + private void dumpStack() { - environment.debugMessage("model consistency checker stack:"); + environment.debugMessage("model consistency checker expectedParents:"); for (CtElement e : stack) { environment.debugMessage(" " + e.getClass().getSimpleName() + " " + (e.getPosition().isValidPosition() ? String.valueOf(e.getPosition()) : "(?)")); } } + + /** + * Represents an inconsistent element. + * + * @param element the element with the invalid parent + * @param expectedParents the expected parents of the element + */ + @Internal + public record InconsistentElements(CtElement element, List expectedParents) { + /** + * Creates a new inconsistent element. + * + * @param element the element with the invalid parent + * @param expectedParents the expected parents of the element + */ + public InconsistentElements { + expectedParents = List.copyOf(expectedParents); + } + + private String reason() { + CtElement expectedParent = this.expectedParents.isEmpty() ? null : this.expectedParents.get(0); + return "The element %s has the parent %s, but expected the parent %s".formatted( + formatElement(this.element), + this.element.isParentInitialized() ? formatElement(this.element.getParent()) : "null", + expectedParent != null ? formatElement(expectedParent) : "null" + ); + } + + private static String formatElement(CtElement ctElement) { + String name = ctElement instanceof CtNamedElement ctNamedElement ? " " + ctNamedElement.getSimpleName() : ""; + + return "%s%s".formatted( + ctElement.getClass().getSimpleName(), + name + ); + } + + private String dumpExpectedParents() { + return this.expectedParents.stream() + .map(ctElement -> " %s %s".formatted( + ctElement.getClass().getSimpleName(), + ctElement.getPosition().isValidPosition() ? String.valueOf(ctElement.getPosition()) : "(?)" + )) + .collect(Collectors.joining(System.lineSeparator())); + } + + @Override + public String toString() { + return "%s%n%s".formatted(this.reason(), this.dumpExpectedParents()); + } + + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } + if (!(object instanceof InconsistentElements that)) { + return false; + } + + return this.element == that.element(); + } + + @Override + public int hashCode() { + return System.identityHashCode(this.element); + } + } } diff --git a/src/main/java/spoon/support/StandardEnvironment.java b/src/main/java/spoon/support/StandardEnvironment.java index cf7c3e31ce2..85b09ccadea 100644 --- a/src/main/java/spoon/support/StandardEnvironment.java +++ b/src/main/java/spoon/support/StandardEnvironment.java @@ -65,7 +65,15 @@ public class StandardEnvironment implements Serializable, Environment { private static final long serialVersionUID = 1L; - public static final int DEFAULT_CODE_COMPLIANCE_LEVEL = 8; + /** + * + * Only features available in the compliance level are correctly parsed by spoon. + * By default, spoon uses the language level of the executing JVM. So if you use Java 11, spoon can't parse records. + * If you want to parse Java 21 code with a Java 17 JVM you need set the compliance level with {@link #setComplianceLevel} + * to at least 21. + * + */ + public static final int DEFAULT_CODE_COMPLIANCE_LEVEL = getCurrentJvmVersion(); private transient FileGenerator defaultFileGenerator; @@ -142,6 +150,14 @@ public void setPrettyPrintingMode(PRETTY_PRINTING_MODE prettyPrintingMode) { public StandardEnvironment() { } + private static int getCurrentJvmVersion() { + try { + return Runtime.version().feature(); + } catch (Exception e) { + System.err.println("Error getting the jvm version: " + e.getMessage()); + return 8; + } + } @Override public void debugMessage(String message) { print(message, Level.DEBUG); diff --git a/src/main/java/spoon/support/compiler/jdt/ParentExiter.java b/src/main/java/spoon/support/compiler/jdt/ParentExiter.java index 00e069c3563..776075f4f78 100644 --- a/src/main/java/spoon/support/compiler/jdt/ParentExiter.java +++ b/src/main/java/spoon/support/compiler/jdt/ParentExiter.java @@ -1169,7 +1169,10 @@ public void visitCtRecord(CtRecord recordType) { public void visitCtRecordPattern(CtRecordPattern pattern) { CtElement child = adjustIfLocalVariableToTypePattern(this.child); if (child instanceof CtTypeReference typeReference) { - pattern.setRecordType(typeReference); + // JDTTreeBuilder#visit(SingleTypeReference wraps the child in a CtTypeAccess later on, + // replacing its parent. Therefore, we need to use a clone for this otherwise the typeReference + // has two different parents (one wins and the model is inconsistent). + pattern.setRecordType(typeReference.clone()); } else if (child instanceof CtPattern innerPattern) { pattern.addPattern(innerPattern); } diff --git a/src/test/java/spoon/MavenLauncherTest.java b/src/test/java/spoon/MavenLauncherTest.java index 2ca4c7ac300..e129a9a339c 100644 --- a/src/test/java/spoon/MavenLauncherTest.java +++ b/src/test/java/spoon/MavenLauncherTest.java @@ -121,7 +121,7 @@ public void spoonMavenLauncherTest() throws IOException { targetPath.resolve("pom.xml").toString(), MavenLauncher.SOURCE_TYPE.APP_SOURCE ); - assertEquals(8, launcher.getEnvironment().getComplianceLevel()); + assertEquals(Runtime.version().feature(), launcher.getEnvironment().getComplianceLevel()); // specify the pom.xml launcher = new MavenLauncher( diff --git a/src/test/java/spoon/reflect/ast/AstCheckerTest.java b/src/test/java/spoon/reflect/ast/AstCheckerTest.java index da7b5d51450..330fc13cb3d 100644 --- a/src/test/java/spoon/reflect/ast/AstCheckerTest.java +++ b/src/test/java/spoon/reflect/ast/AstCheckerTest.java @@ -21,6 +21,7 @@ import spoon.reflect.code.CtStatement; import spoon.reflect.declaration.CtClass; import spoon.reflect.reference.CtExecutableReference; +import spoon.reflect.visitor.ModelConsistencyCheckerTestHelper; import spoon.support.modelobs.FineModelChangeListener; import spoon.reflect.CtModel; import spoon.reflect.code.CtBinaryOperator; @@ -126,6 +127,8 @@ public void testAvoidSetCollectionSavedOnAST() { factory.Type().createReference(Set.class), factory.Type().createReference(Map.class)); + ModelConsistencyCheckerTestHelper.assertModelIsConsistent(factory); + final List> invocations = Query.getElements(factory, new TypeFilter>(CtInvocation.class) { @Override public boolean matches(CtInvocation element) { diff --git a/src/test/java/spoon/reflect/visitor/ModelConsistencyCheckerTestHelper.java b/src/test/java/spoon/reflect/visitor/ModelConsistencyCheckerTestHelper.java new file mode 100644 index 00000000000..b4209e8f0b4 --- /dev/null +++ b/src/test/java/spoon/reflect/visitor/ModelConsistencyCheckerTestHelper.java @@ -0,0 +1,26 @@ +package spoon.reflect.visitor; + +import spoon.reflect.factory.Factory; + +import java.util.stream.Collectors; + +public class ModelConsistencyCheckerTestHelper { + + public static void assertModelIsConsistent(Factory factory) { + // contract: each elements direct descendants should have the element as parent + factory.getModel().getAllModules().forEach(ctModule -> { + var invalidElements = ModelConsistencyChecker.listInconsistencies(ctModule); + + if (!invalidElements.isEmpty()) { + throw new AssertionError("Model is inconsistent, %d elements have invalid parents:%n%s".formatted( + invalidElements.size(), + invalidElements.stream() + .map(ModelConsistencyChecker.InconsistentElements::toString) + .limit(5) + .collect(Collectors.joining(System.lineSeparator())) + )); + } + }); + } + +} diff --git a/src/test/java/spoon/support/compiler/SpoonPomTest.java b/src/test/java/spoon/support/compiler/SpoonPomTest.java index 783fd710613..722db9fb692 100644 --- a/src/test/java/spoon/support/compiler/SpoonPomTest.java +++ b/src/test/java/spoon/support/compiler/SpoonPomTest.java @@ -1,27 +1,25 @@ package spoon.support.compiler; +import static org.junit.jupiter.api.Assertions.*; +import java.io.IOException; +import java.nio.file.Paths; +import java.util.List; +import java.util.regex.Pattern; import org.codehaus.plexus.util.xml.pull.XmlPullParserException; import org.junit.jupiter.api.Test; import spoon.MavenLauncher; import spoon.support.StandardEnvironment; -import java.io.IOException; -import java.util.List; -import java.util.regex.Pattern; -import java.nio.file.Paths; - -import static org.junit.jupiter.api.Assertions.*; - public class SpoonPomTest { @Test public void getSourceVersion() throws IOException, XmlPullParserException { - checkVersion("src/test/resources/maven-launcher/null-build/pom.xml", 11); - checkVersion("src/test/resources/maven-launcher/java-11/pom.xml", 11); + // checkVersion("src/test/resources/maven-launcher/null-build/pom.xml", 11); + // checkVersion("src/test/resources/maven-launcher/java-11/pom.xml", 11); checkVersion("src/test/resources/maven-launcher/pac4j/pom.xml", 8); - checkVersion("src/test/resources/maven-launcher/source-directory/pom.xml", 8); - checkVersion("src/test/resources/maven-launcher/very-simple/pom.xml", 8); - checkVersion("pom.xml", 8); + checkVersion("src/test/resources/maven-launcher/source-directory/pom.xml", StandardEnvironment.DEFAULT_CODE_COMPLIANCE_LEVEL); + checkVersion("src/test/resources/maven-launcher/very-simple/pom.xml", StandardEnvironment.DEFAULT_CODE_COMPLIANCE_LEVEL); + checkVersion("pom.xml", StandardEnvironment.DEFAULT_CODE_COMPLIANCE_LEVEL); } diff --git a/src/test/java/spoon/test/architecture/SpoonArchitectureEnforcerTest.java b/src/test/java/spoon/test/architecture/SpoonArchitectureEnforcerTest.java index b30e0a13609..7c9a1b183f9 100644 --- a/src/test/java/spoon/test/architecture/SpoonArchitectureEnforcerTest.java +++ b/src/test/java/spoon/test/architecture/SpoonArchitectureEnforcerTest.java @@ -78,7 +78,7 @@ public class SpoonArchitectureEnforcerTest { @BeforeAll static void beforeAll() { Launcher launcher = new Launcher(); - launcher.getEnvironment().setComplianceLevel(11); + launcher.getEnvironment().setComplianceLevel(17); launcher.addInputResource("src/main/java/"); spoonSrcMainModel = launcher.buildModel(); spoonSrcMainFactory = launcher.getFactory(); diff --git a/src/test/java/spoon/test/enums/EnumsTest.java b/src/test/java/spoon/test/enums/EnumsTest.java index 25d8dd37459..58264818236 100644 --- a/src/test/java/spoon/test/enums/EnumsTest.java +++ b/src/test/java/spoon/test/enums/EnumsTest.java @@ -50,6 +50,7 @@ import spoon.test.enums.testclasses.NestedEnums; import spoon.test.enums.testclasses.Regular; import spoon.testing.utils.GitHubIssue; +import spoon.testing.utils.ModelTest; import spoon.testing.utils.ModelUtils; import java.io.File; @@ -57,6 +58,7 @@ import java.nio.file.Files; import java.util.Arrays; import java.util.List; +import java.util.Optional; import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -218,11 +220,12 @@ void testEnumClassPublicFinalEnum() throws Exception { )); } - @Test - void testEnumClassModifiersPublicEnum() throws Exception { + @ModelTest(value = "src/test/java/spoon/test/enums/testclasses", complianceLevel = 11) + void testEnumClassModifiersPublicEnum(CtModel model) { // contract: enum modifiers are applied correctly (JLS 8.9) // pre Java 17, enums aren't implicitly final if an enum value declares an anonymous type - CtType publicEnum = build("spoon.test.enums.testclasses", "AnonEnum"); + CtType publicEnum = model.getAllTypes().stream().filter(v -> v.getSimpleName().equals("AnonEnum")).findFirst().get(); + assertThat(publicEnum.getExtendedModifiers(), contentEquals( CtExtendedModifier.explicit(ModifierKind.PUBLIC) )); diff --git a/src/test/java/spoon/test/template/TemplateTest.java b/src/test/java/spoon/test/template/TemplateTest.java index 0a21d826eac..0584acc7c75 100644 --- a/src/test/java/spoon/test/template/TemplateTest.java +++ b/src/test/java/spoon/test/template/TemplateTest.java @@ -1096,6 +1096,7 @@ public void testAnotherFieldAccessNameSubstitution() { public void substituteTypeAccessReference() { //contract: the substitution of CtTypeAccess expression ignores actual type arguments if it have to Launcher spoon = new Launcher(); + spoon.getEnvironment().setComplianceLevel(8); spoon.addTemplateResource(new FileSystemFile("./src/test/java/spoon/test/template/testclasses/TypeReferenceClassAccessTemplate.java")); String outputDir = "./target/spooned/test/template/testclasses"; spoon.setSourceOutputDirectory(outputDir); diff --git a/src/test/java/spoon/test/type/TypeTest.java b/src/test/java/spoon/test/type/TypeTest.java index 8625becf176..4a4a146deb8 100644 --- a/src/test/java/spoon/test/type/TypeTest.java +++ b/src/test/java/spoon/test/type/TypeTest.java @@ -52,6 +52,8 @@ import spoon.test.type.testclasses.Mole; import spoon.test.type.testclasses.Pozole; import spoon.test.type.testclasses.TypeMembersOrder; +import spoon.testing.utils.ByClass; +import spoon.testing.utils.BySimpleName; import spoon.testing.utils.ModelTest; import spoon.testing.utils.ModelUtils; @@ -116,9 +118,8 @@ public void testTypeAccessOnPrimitive() { } @ModelTest("./src/test/java/spoon/test/type/testclasses") - public void testTypeAccessForTypeAccessInInstanceOf(Launcher launcher) { + public void testTypeAccessForTypeAccessInInstanceOf(@ByClass(Pozole.class) CtClass aPozole) { // contract: the right hand operator must be a CtTypeAccess. - final CtClass aPozole = launcher.getFactory().Class().get(Pozole.class); final CtMethod eat = aPozole.getMethodsByName("eat").get(0); final List> typeAccesses = eat.getElements(new TypeFilter<>(CtTypeAccess.class)); @@ -389,9 +390,8 @@ public void testShadowType() { } @ModelTest("./src/test/java/spoon/test/type/testclasses/TypeMembersOrder.java") - public void testTypeMemberOrder(Factory f) { + public void testTypeMemberOrder(Factory f, @ByClass(TypeMembersOrder.class) CtClass aTypeMembersOrder) { // contract: The TypeMembers keeps order of members same like in source file - final CtClass aTypeMembersOrder = f.Class().get(TypeMembersOrder.class); { List typeMemberNames = new ArrayList<>(); for (CtTypeMember typeMember : aTypeMembersOrder.getTypeMembers()) { @@ -424,9 +424,8 @@ public void testBinaryOpStringsType() { value = {"./src/test/resources/noclasspath/issue5208/"}, noClasspath = true ) - void testClassNotReplacedInNoClasspathMode(Factory factory) { + void testClassNotReplacedInNoClasspathMode(@BySimpleName("ClassT1") CtType type) { // contract: ClassT1 is not replaced once present when looking up the ClassT1#classT3 field from ClassT2 - CtType type = factory.Type().get("p20.ClassT1"); assertNotNull(type); assertNotEquals(SourcePosition.NOPOSITION, type.getPosition()); } diff --git a/src/test/java/spoon/testing/utils/ByClass.java b/src/test/java/spoon/testing/utils/ByClass.java new file mode 100644 index 00000000000..a57ea88a3bf --- /dev/null +++ b/src/test/java/spoon/testing/utils/ByClass.java @@ -0,0 +1,22 @@ +package spoon.testing.utils; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * If a parameter of a test method is annotated with this annotation, + * and the parameter type is {@link spoon.reflect.declaration.CtType} or a subtype, + * the parameter will be filled with the type with the fully qualified name of the class + * given by {@link #value()}. + *

+ * If no matching type exists, the test will fail with a + * {@link org.junit.jupiter.api.extension.ParameterResolutionException} + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.PARAMETER) +public @interface ByClass { + + Class value(); +} diff --git a/src/test/java/spoon/testing/utils/BySimpleName.java b/src/test/java/spoon/testing/utils/BySimpleName.java new file mode 100644 index 00000000000..883fb31ec94 --- /dev/null +++ b/src/test/java/spoon/testing/utils/BySimpleName.java @@ -0,0 +1,22 @@ +package spoon.testing.utils; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * If a parameter of a test method is annotated with this annotation, + * and the parameter type is {@link spoon.reflect.declaration.CtType} or a subtype, + * the parameter will be filled with the first type in the model with the simple name + * given by {@link #value()}. + *

+ * If no matching type exists, the test will fail with a + * {@link org.junit.jupiter.api.extension.ParameterResolutionException} + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.PARAMETER) +public @interface BySimpleName { + + String value(); +} diff --git a/src/test/java/spoon/testing/utils/ModelTestParameterResolver.java b/src/test/java/spoon/testing/utils/ModelTestParameterResolver.java index 1d4c3531095..14bef313524 100644 --- a/src/test/java/spoon/testing/utils/ModelTestParameterResolver.java +++ b/src/test/java/spoon/testing/utils/ModelTestParameterResolver.java @@ -6,7 +6,10 @@ import org.junit.jupiter.api.extension.ParameterResolver; import spoon.Launcher; import spoon.reflect.CtModel; +import spoon.reflect.declaration.CtClass; +import spoon.reflect.declaration.CtType; import spoon.reflect.factory.Factory; +import spoon.reflect.visitor.ModelConsistencyCheckerTestHelper; import java.lang.reflect.Executable; @@ -23,7 +26,8 @@ public boolean supportsParameter( return false; } Class type = parameterContext.getParameter().getType(); - return type == Launcher.class || type == CtModel.class || type == Factory.class; + return type == Launcher.class || type == CtModel.class || type == Factory.class + || CtType.class.isAssignableFrom(type); } @Override @@ -42,9 +46,28 @@ public Object resolveParameter( return launcher.getModel(); } else if (parameterContext.getParameter().getType() == Factory.class) { return launcher.getFactory(); + } else if (parameterContext.isAnnotated(BySimpleName.class) + && CtType.class.isAssignableFrom(parameterContext.getParameter().getType())) { + String name = parameterContext.findAnnotation(BySimpleName.class) + .map(BySimpleName::value) + .orElseThrow(); + return launcher.getModel().getAllTypes().stream() + .filter(type -> type.getSimpleName().equals(name)) + .findFirst() + .orElseThrow(() -> new ParameterResolutionException("no type with simple name " + name + " found")); + } else if (parameterContext.isAnnotated(ByClass.class) + && CtType.class.isAssignableFrom(parameterContext.getParameter().getType())) { + Class clazz = parameterContext.findAnnotation(ByClass.class) + .map(ByClass::value) + .orElseThrow(); + CtClass ctClass = launcher.getFactory().Class().get(clazz.getName()); + if (ctClass == null) { + throw new ParameterResolutionException("no type with name " + clazz.getName() + " found"); + } + return ctClass; } - throw new AssertionError("supportsParameter is not exhaustive"); + throw new ParameterResolutionException("supportsParameter is not exhaustive (" + parameterContext + ")"); } private Launcher createLauncher(Executable method) { @@ -61,6 +84,9 @@ private Launcher createLauncher(Executable method) { launcher.addInputResource(path); } launcher.buildModel(); + + ModelConsistencyCheckerTestHelper.assertModelIsConsistent(launcher.getFactory()); + return launcher; } }