diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index 3de59fe1c3b..00000000000 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,36 +0,0 @@ ---- -name: Bug report -about: Create a report to help us improve -title: "[Bug] Bug description" -labels: '' -assignees: '' - ---- - -**Describe the bug** -A clear and concise description of what the bug is. - -**To Reproduce** - -Input: -```java -/*Code being processed by spoon and leading to the bug.*/ -``` - -Processing with Spoon: -```java -/*Code calling spoon on the input and triggering the bug.*/ -``` - -Output: -```java -/*Output code or stacktrace if unexpected Exception is raised*/ -``` - -Note that you can also open a pull request reproducing the issue and reference that, instead of writing additional information here. - -**Operating system, JDK and Spoon version used** - -* OS: -* JDK: -* Spoon version: diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml new file mode 100644 index 00000000000..542c11fe316 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -0,0 +1,83 @@ +name: Bug Report +description: File a bug report +title: "[Bug]: " +labels: ["bug"] +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to fill out this bug report! + - type: textarea + id: bug-description + attributes: + label: Describe the bug + description: Provide a general description of the bug, in plain text. Use the dedicated fields below for code snippets. + placeholder: Description of the bug + validations: + required: true + - type: textarea + id: java-input + attributes: + label: Source code you are trying to analyze/transform + description: Provide a minimal example of source code you are trying to analyze/transform that causes the bug to appear. This will be automatically formatted, so no need for backticks. + placeholder: System.out.println("Hello, world!"); + render: Java + validations: + required: false + - type: textarea + id: spoon-code + attributes: + label: Source code for your Spoon processing + description: Provide your processing code that uses Spoon and causes the bug to appear. This will be automatically formatted, so no need for backticks. + placeholder: Launcher launcher = new Launcher(); + render: Java + validations: + required: false + - type: textarea + id: output + attributes: + label: Actual output + description: The output that spoon provides, or a stacktrace if it crashes. This will be automatically formatted, so no need for backticks. + placeholder: System.out.println("Hello, world!"); + render: Java + validations: + required: false + - type: textarea + id: expected-output + attributes: + label: Expected output + description: If you know what kind of output you expect to get, please provide it here. This will be automatically formatted, so no need for backticks. + placeholder: System.out.println("Hello, world!"); + render: Java + validations: + required: false + - type: textarea + id: version + attributes: + label: Spoon Version + description: What version of Spoon are you using? Please note that we only actively support the latest major release. If you are using an older major version, your issue may be closed as non-actionable. + placeholder: X.Y.Z + validations: + required: true + - type: dropdown + id: jvm-version + attributes: + label: JVM Version + description: Which JVM version are you running Spoon with? Note that Spoon is built for Java 11+, and we cannot maintain support for older versions. + options: + - "11" + - "12" + - "13" + - "14" + - "15" + - "16" + - "17" + - "18" + validations: + required: true + - type: textarea + id: os + attributes: + label: What operating system are you using? + validations: + required: true \ No newline at end of file diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 5a655bc0a4a..2504dd222a8 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -38,7 +38,7 @@ jobs: - name: Disable Git's autocrlf run: git config --global core.autocrlf false - uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b # tag=v3.0.2 - - uses: actions/setup-java@860f60056505705214d223b91ed7a30f173f6142 # tag=v3.3.0 + - uses: actions/setup-java@2c7a4878f5d120bd643426d54ae1209b29cc01a3 # tag=v3.4.1 with: java-version: ${{ matrix.java }} distribution: ${{ env.JAVA_DISTRIBUTION }} @@ -48,7 +48,7 @@ jobs: run: echo "::set-output name=date::$(/bin/date -u "+%Y%m%d")" shell: bash - name: Use Maven dependency cache - uses: actions/cache@c3f1317a9e7b1ef106c153ac8c0f00fed3ddbc0d # tag=v3.0.4 + uses: actions/cache@0865c47f36e68161719c5b124609996bb5c40129 # tag=v3.0.5 with: path: ~/.m2/repository key: ${{ runner.os }}-${{ steps.get-date.outputs.date }}-${{ hashFiles('**/pom.xml') }} @@ -75,7 +75,7 @@ jobs: name: Test with coverage steps: - uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b # tag=v3.0.2 - - uses: actions/setup-java@860f60056505705214d223b91ed7a30f173f6142 # tag=v3.3.0 + - uses: actions/setup-java@2c7a4878f5d120bd643426d54ae1209b29cc01a3 # tag=v3.4.1 with: java-version: 17 distribution: ${{ env.JAVA_DISTRIBUTION }} @@ -85,7 +85,7 @@ jobs: run: echo "::set-output name=date::$(/bin/date -u "+%Y%m%d")" shell: bash - name: Use Maven dependency cache - uses: actions/cache@c3f1317a9e7b1ef106c153ac8c0f00fed3ddbc0d # tag=v3.0.4 + uses: actions/cache@0865c47f36e68161719c5b124609996bb5c40129 # tag=v3.0.5 with: path: ~/.m2/repository key: ${{ runner.os }}-${{ steps.get-date.outputs.date }}-${{ hashFiles('**/pom.xml') }} @@ -111,11 +111,11 @@ jobs: - uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b # tag=v3.0.2 with: fetch-depth: 0 - - uses: actions/setup-java@860f60056505705214d223b91ed7a30f173f6142 # tag=v3.3.0 + - uses: actions/setup-java@2c7a4878f5d120bd643426d54ae1209b29cc01a3 # tag=v3.4.1 with: java-version: 17 distribution: ${{ env.JAVA_DISTRIBUTION }} - - uses: actions/setup-python@d09bd5e6005b175076f227b13d9730d56e9dcfcb # tag=v4.0.0 + - uses: actions/setup-python@c4e89fac7e8767b327bbad6cb4d859eda999cf08 # tag=v4.1.0 with: python-version: 3.6 @@ -124,7 +124,7 @@ jobs: run: echo "::set-output name=date::$(/bin/date -u "+%Y%m%d")" shell: bash - name: Use Maven dependency cache - uses: actions/cache@c3f1317a9e7b1ef106c153ac8c0f00fed3ddbc0d # tag=v3.0.4 + uses: actions/cache@0865c47f36e68161719c5b124609996bb5c40129 # tag=v3.0.5 with: path: ~/.m2/repository key: ${{ runner.os }}-${{ steps.get-date.outputs.date }}-${{ hashFiles('**/pom.xml') }} @@ -148,14 +148,14 @@ jobs: uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b # tag=v3.0.2 with: path: pull_request - - uses: actions/setup-java@860f60056505705214d223b91ed7a30f173f6142 # tag=v3.3.0 + - uses: actions/setup-java@2c7a4878f5d120bd643426d54ae1209b29cc01a3 # tag=v3.4.1 with: java-version: 17 distribution: ${{ env.JAVA_DISTRIBUTION }} - name: git reset run: cd pull_request && git fetch && git reset --soft origin/master - name: Qodana - Code Inspection - uses: JetBrains/qodana-action@b06a3f2289e63df0b6746cfd80f5af9fb6a6c65a # tag=v5.1.0 + uses: JetBrains/qodana-action@102d021fe8a24d8af48a38a35f96b8ba100baa20 # tag=v2022.1.1 with: linter: ${{ env.QODANA_LINTER }} project-dir: "${{ github.workspace }}/pull_request" @@ -165,7 +165,7 @@ jobs: fail-threshold: 0 use-annotations: false use-caches: false # we disable cache for consistent results - - uses: github/codeql-action/upload-sarif@27ea8f8fe5977c00f5b37e076ab846c5bd783b96 # tag=v2 + - uses: github/codeql-action/upload-sarif@3f62b754e23e0dd60f91b744033e1dc1654c0ec6 # tag=v2 if: always() with: sarif_file: "${{ github.workspace }}/result/qodana.sarif.json" @@ -179,7 +179,7 @@ jobs: - uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b # tag=v3.0.2 with: fetch-depth: 0 - - uses: actions/setup-java@860f60056505705214d223b91ed7a30f173f6142 # tag=v3.3.0 + - uses: actions/setup-java@2c7a4878f5d120bd643426d54ae1209b29cc01a3 # tag=v3.4.1 with: java-version: 17 distribution: ${{ env.JAVA_DISTRIBUTION }} diff --git a/README.md b/README.md index 25f9c378dad..499eb282b74 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,9 @@ If you need professional support on Spoon (development, training, extension), yo ## Getting started in 2 seconds -> **Java version:** Spoon version 10 and up requires Java 11 or later. Spoon 9.1.0 is the final Spoon release compatible with Java 8. +> **Java version:** Spoon version 10 and up requires Java 11 or later. Spoon 9.1.0 is the final Spoon release compatible +> with Java 8, and we do not plan to backport any bug fixes or features to Spoon 9. Note that Spoon can of course still +> consume source code for older versions of Java, but it needs JDK 11+ to run. Get latest stable version with Maven, see diff --git a/pom.xml b/pom.xml index a7c0192eb7e..972c12abb42 100644 --- a/pom.xml +++ b/pom.xml @@ -48,7 +48,7 @@ org.eclipse.jdt org.eclipse.jdt.core - 3.29.0 + 3.30.0 org.eclipse.platform @@ -227,7 +227,7 @@ org.apache.maven.plugins maven-assembly-plugin - 3.3.0 + 3.4.1 diff --git a/spoon-decompiler/pom.xml b/spoon-decompiler/pom.xml index c9f3dbb8da5..dabea95c37a 100644 --- a/spoon-decompiler/pom.xml +++ b/spoon-decompiler/pom.xml @@ -56,7 +56,7 @@ org.apache.maven.plugins maven-assembly-plugin - 3.3.0 + 3.4.1 diff --git a/spoon-pom/pom.xml b/spoon-pom/pom.xml index e761bc287ae..a3bca6573b1 100644 --- a/spoon-pom/pom.xml +++ b/spoon-pom/pom.xml @@ -173,7 +173,7 @@ org.apache.maven.wagon wagon-ssh - 3.5.1 + 3.5.2 diff --git a/spoon-visualisation/src/test/java/spoon/visualisation/instrument/SpoonCodeInstrumentTest.java b/spoon-visualisation/src/test/java/spoon/visualisation/instrument/SpoonCodeInstrumentTest.java index 129b38fa6e9..428f711e95f 100644 --- a/spoon-visualisation/src/test/java/spoon/visualisation/instrument/SpoonCodeInstrumentTest.java +++ b/spoon-visualisation/src/test/java/spoon/visualisation/instrument/SpoonCodeInstrumentTest.java @@ -56,6 +56,7 @@ import spoon.reflect.cu.SourcePositionHolder; import spoon.reflect.declaration.CtClass; import spoon.reflect.declaration.CtElement; +import spoon.reflect.declaration.CtSealable; import spoon.reflect.declaration.CtType; import spoon.reflect.path.CtPath; import spoon.reflect.visitor.CtVisitable; @@ -232,7 +233,7 @@ void testSaveText(final FxRobot robot, @TempDir final Path tempDir) throws IOExc @Test void getDirectSpoonInterfacesOfClass() { final List> inter = spoonCodeInstrument.getDirectSpoonInterfaces(CtClass.class).collect(Collectors.toList()); - assertThat(inter).containsExactlyInAnyOrder(CtType.class, CtStatement.class); + assertThat(inter).containsExactlyInAnyOrder(CtType.class, CtStatement.class, CtSealable.class); } @Test diff --git a/src/main/java/spoon/metamodel/Metamodel.java b/src/main/java/spoon/metamodel/Metamodel.java index 9eb6d82f97a..430fd06503b 100644 --- a/src/main/java/spoon/metamodel/Metamodel.java +++ b/src/main/java/spoon/metamodel/Metamodel.java @@ -152,6 +152,7 @@ public static Set> getAllMetamodelInterfaces() { result.add(factory.Type().get(spoon.reflect.declaration.CtNamedElement.class)); result.add(factory.Type().get(spoon.reflect.declaration.CtPackage.class)); result.add(factory.Type().get(spoon.reflect.declaration.CtParameter.class)); + result.add(factory.Type().get(spoon.reflect.declaration.CtSealable.class)); result.add(factory.Type().get(spoon.reflect.declaration.CtShadowable.class)); result.add(factory.Type().get(spoon.reflect.declaration.CtType.class)); result.add(factory.Type().get(spoon.reflect.declaration.CtTypeInformation.class)); diff --git a/src/main/java/spoon/reflect/declaration/CtClass.java b/src/main/java/spoon/reflect/declaration/CtClass.java index f8dedad0b9e..02d4948031b 100644 --- a/src/main/java/spoon/reflect/declaration/CtClass.java +++ b/src/main/java/spoon/reflect/declaration/CtClass.java @@ -31,7 +31,7 @@ * * @author Renaud Pawlak */ -public interface CtClass extends CtType, CtStatement { +public interface CtClass extends CtType, CtStatement, CtSealable { /** * Returns the anonymous blocks of this class. * Derived from {@link #getTypeMembers()} diff --git a/src/main/java/spoon/reflect/declaration/CtEnum.java b/src/main/java/spoon/reflect/declaration/CtEnum.java index 994a4fa46b5..a977307af30 100644 --- a/src/main/java/spoon/reflect/declaration/CtEnum.java +++ b/src/main/java/spoon/reflect/declaration/CtEnum.java @@ -10,9 +10,12 @@ import spoon.reflect.reference.CtTypeReference; import spoon.reflect.annotations.PropertyGetter; import spoon.reflect.annotations.PropertySetter; +import spoon.support.DerivedProperty; import spoon.support.UnsettableProperty; +import java.util.Collection; import java.util.List; +import java.util.Set; import static spoon.reflect.path.CtRole.VALUE; @@ -80,4 +83,20 @@ public interface CtEnum> extends CtClass { @Override @UnsettableProperty > C setSuperclass(CtTypeReference superClass); + + @Override + @DerivedProperty + Set> getPermittedTypes(); + + @Override + @UnsettableProperty + CtEnum setPermittedTypes(Collection> permittedTypes); + + @Override + @UnsettableProperty + CtEnum addPermittedType(CtTypeReference type); + + @Override + @UnsettableProperty + CtEnum removePermittedType(CtTypeReference type); } diff --git a/src/main/java/spoon/reflect/declaration/CtInterface.java b/src/main/java/spoon/reflect/declaration/CtInterface.java index 8f4bf8ca480..ac3d58d6990 100644 --- a/src/main/java/spoon/reflect/declaration/CtInterface.java +++ b/src/main/java/spoon/reflect/declaration/CtInterface.java @@ -21,7 +21,7 @@ * } * */ -public interface CtInterface extends CtType, CtStatement { +public interface CtInterface extends CtType, CtStatement, CtSealable { @Override CtInterface clone(); diff --git a/src/main/java/spoon/reflect/declaration/CtRecord.java b/src/main/java/spoon/reflect/declaration/CtRecord.java index 5b0cf8eb78d..2921f0575b0 100644 --- a/src/main/java/spoon/reflect/declaration/CtRecord.java +++ b/src/main/java/spoon/reflect/declaration/CtRecord.java @@ -7,6 +7,7 @@ */ package spoon.reflect.declaration; +import java.util.Collection; import java.util.Set; import spoon.reflect.annotations.PropertyGetter; import spoon.reflect.annotations.PropertySetter; @@ -40,4 +41,19 @@ public interface CtRecord extends CtClass { @Override @UnsettableProperty > C setSuperclass(CtTypeReference superClass); + + @Override + Set> getPermittedTypes(); + + @Override + @UnsettableProperty + CtRecord setPermittedTypes(Collection> permittedTypes); + + @Override + @UnsettableProperty + CtRecord addPermittedType(CtTypeReference type); + + @Override + @UnsettableProperty + CtRecord removePermittedType(CtTypeReference type); } diff --git a/src/main/java/spoon/reflect/declaration/CtSealable.java b/src/main/java/spoon/reflect/declaration/CtSealable.java new file mode 100644 index 00000000000..091b116b712 --- /dev/null +++ b/src/main/java/spoon/reflect/declaration/CtSealable.java @@ -0,0 +1,62 @@ +/* + * SPDX-License-Identifier: (MIT OR CECILL-C) + * + * Copyright (C) 2006-2019 INRIA and contributors + * + * Spoon is available either under the terms of the MIT License (see LICENSE-MIT.txt) of the Cecill-C License (see LICENSE-CECILL-C.txt). You as the user are entitled to choose the terms under which to adopt Spoon. + */ +package spoon.reflect.declaration; + +import spoon.reflect.annotations.PropertyGetter; +import spoon.reflect.annotations.PropertySetter; +import spoon.reflect.path.CtRole; +import spoon.reflect.reference.CtTypeReference; + +import java.util.Collection; +import java.util.Set; + +/** + * This interface represents any type that can be sealed. + * See JLS 8.1.1.2 + */ +public interface CtSealable { + + /** + * Returns the permitted types for this type. + * + * @return an unmodifiable view of the permitted types. + */ + @PropertyGetter(role = CtRole.PERMITTED_TYPE) + Set> getPermittedTypes(); + + /** + * Sets the permitted types for this type. + * Calling this method does not change the state of the {@link ModifierKind#SEALED} for this type. + * The previously permitted types will be removed. + * + * @param permittedTypes the permitted types to set. + * @return this. + */ + @PropertySetter(role = CtRole.PERMITTED_TYPE) + CtSealable setPermittedTypes(Collection> permittedTypes); + + /** + * Adds a permitted type to this type. + * Calling this method does not change the state of the {@link ModifierKind#SEALED} for this type. + * + * @param type the type to add as permitted type. + * @return this. + */ + @PropertySetter(role = CtRole.PERMITTED_TYPE) + CtSealable addPermittedType(CtTypeReference type); + + /** + * Adds a permitted type to this type. + * Calling this method does not change the state of the {@link ModifierKind#SEALED} for this type. + * + * @param type the type to remove from this type's permitted types. + * @return this. + */ + @PropertySetter(role = CtRole.PERMITTED_TYPE) + CtSealable removePermittedType(CtTypeReference type); +} diff --git a/src/main/java/spoon/reflect/declaration/ModifierKind.java b/src/main/java/spoon/reflect/declaration/ModifierKind.java index 5c5180baa91..b75ee78d217 100644 --- a/src/main/java/spoon/reflect/declaration/ModifierKind.java +++ b/src/main/java/spoon/reflect/declaration/ModifierKind.java @@ -20,58 +20,67 @@ public enum ModifierKind { /** * The modifier public */ - PUBLIC, + PUBLIC("public"), /** * The modifier protected */ - PROTECTED, + PROTECTED("protected"), /** * The modifier private */ - PRIVATE, + PRIVATE("private"), /** * The modifier abstract */ - ABSTRACT, + ABSTRACT("abstract"), /** * The modifier static */ - STATIC, + STATIC("static"), /** * The modifier final */ - FINAL, + FINAL("final"), /** * The modifier transient */ - TRANSIENT, + TRANSIENT("transient"), /** * The modifier volatile */ - VOLATILE, + VOLATILE("volatile"), /** * The modifier synchronized */ - SYNCHRONIZED, + SYNCHRONIZED("synchronized"), /** * The modifier native */ - NATIVE, + NATIVE("native"), /** * The modifier strictfp */ - STRICTFP; + STRICTFP("strictfp"), + /** + * The modifier non-sealed + */ + NON_SEALED("non-sealed"), + /** + * The modifier sealed + */ + SEALED("sealed"); + + ModifierKind(String lowercase) { + this.lowercase = lowercase; + } - private String lowercase = null; // modifier name in lowercase + private final String lowercase; // modifier name in lowercase /** * Returns this modifier's name in lowercase. */ @Override public String toString() { - if (lowercase == null) { - lowercase = name().toLowerCase(java.util.Locale.US); - } return lowercase; } diff --git a/src/main/java/spoon/reflect/path/CtRole.java b/src/main/java/spoon/reflect/path/CtRole.java index 63e2b7054d0..2b0f8d710ef 100644 --- a/src/main/java/spoon/reflect/path/CtRole.java +++ b/src/main/java/spoon/reflect/path/CtRole.java @@ -120,7 +120,8 @@ public enum CtRole { LITERAL_BASE, CASE_KIND, RECORD_COMPONENT, - COMPACT_CONSTRUCTOR; + COMPACT_CONSTRUCTOR, + PERMITTED_TYPE; private final CtRole superRole; private final List subRoles; diff --git a/src/main/java/spoon/reflect/visitor/CtBiScannerDefault.java b/src/main/java/spoon/reflect/visitor/CtBiScannerDefault.java index cc410e8d78b..47a75466b8d 100644 --- a/src/main/java/spoon/reflect/visitor/CtBiScannerDefault.java +++ b/src/main/java/spoon/reflect/visitor/CtBiScannerDefault.java @@ -207,6 +207,7 @@ public void visitCtClass(final spoon.reflect.declaration.CtClass ctClass) biScan(spoon.reflect.path.CtRole.INTERFACE, ctClass.getSuperInterfaces(), other.getSuperInterfaces()); biScan(spoon.reflect.path.CtRole.TYPE_PARAMETER, ctClass.getFormalCtTypeParameters(), other.getFormalCtTypeParameters()); biScan(spoon.reflect.path.CtRole.TYPE_MEMBER, ctClass.getTypeMembers(), other.getTypeMembers()); + biScan(spoon.reflect.path.CtRole.PERMITTED_TYPE, ctClass.getPermittedTypes(), other.getPermittedTypes()); biScan(spoon.reflect.path.CtRole.COMMENT, ctClass.getComments(), other.getComments()); exit(ctClass); } @@ -397,6 +398,7 @@ public void visitCtInterface(final spoon.reflect.declaration.CtInterface biScan(spoon.reflect.path.CtRole.INTERFACE, intrface.getSuperInterfaces(), other.getSuperInterfaces()); biScan(spoon.reflect.path.CtRole.TYPE_PARAMETER, intrface.getFormalCtTypeParameters(), other.getFormalCtTypeParameters()); biScan(spoon.reflect.path.CtRole.TYPE_MEMBER, intrface.getTypeMembers(), other.getTypeMembers()); + biScan(spoon.reflect.path.CtRole.PERMITTED_TYPE, intrface.getPermittedTypes(), other.getPermittedTypes()); biScan(spoon.reflect.path.CtRole.COMMENT, intrface.getComments(), other.getComments()); exit(intrface); } diff --git a/src/main/java/spoon/reflect/visitor/CtInheritanceScanner.java b/src/main/java/spoon/reflect/visitor/CtInheritanceScanner.java index 4283a0cc4b9..8b53f70c796 100644 --- a/src/main/java/spoon/reflect/visitor/CtInheritanceScanner.java +++ b/src/main/java/spoon/reflect/visitor/CtInheritanceScanner.java @@ -103,6 +103,7 @@ import spoon.reflect.declaration.CtPackage; import spoon.reflect.declaration.CtPackageDeclaration; import spoon.reflect.declaration.CtParameter; +import spoon.reflect.declaration.CtSealable; import spoon.reflect.declaration.CtShadowable; import spoon.reflect.declaration.CtType; import spoon.reflect.declaration.CtTypeInformation; @@ -368,6 +369,13 @@ public void scanCtBodyHolder(CtBodyHolder ctBodyHolder) { public void scanCtPattern(CtPattern pattern) { } + /** + * Scans a sealable type + * @param sealable the sealable type to scan + */ + public void scanCtSealable(CtSealable sealable) { + } + @Override public void visitCtFieldRead(CtFieldRead fieldRead) { visitCtVariableRead(fieldRead); @@ -520,6 +528,7 @@ public void visitCtCatch(CtCatch e) { public void visitCtClass(CtClass e) { scanCtType(e); + scanCtSealable(e); scanCtStatement(e); scanCtTypeInformation(e); scanCtFormalTypeDeclarer(e); @@ -655,6 +664,7 @@ public void visitCtIf(CtIf e) { public void visitCtInterface(CtInterface e) { scanCtType(e); + scanCtSealable(e); scanCtStatement(e); scanCtTypeInformation(e); scanCtFormalTypeDeclarer(e); diff --git a/src/main/java/spoon/reflect/visitor/CtScanner.java b/src/main/java/spoon/reflect/visitor/CtScanner.java index e8d4f83637c..3a5a7c6ce2b 100644 --- a/src/main/java/spoon/reflect/visitor/CtScanner.java +++ b/src/main/java/spoon/reflect/visitor/CtScanner.java @@ -355,6 +355,7 @@ public void visitCtClass(final CtClass ctClass) { scan(CtRole.INTERFACE, ctClass.getSuperInterfaces()); scan(CtRole.TYPE_PARAMETER, ctClass.getFormalCtTypeParameters()); scan(CtRole.TYPE_MEMBER, ctClass.getTypeMembers()); + scan(CtRole.PERMITTED_TYPE, ctClass.getPermittedTypes()); scan(CtRole.COMMENT, ctClass.getComments()); exit(ctClass); } @@ -513,6 +514,7 @@ public void visitCtInterface(final CtInterface intrface) { scan(CtRole.INTERFACE, intrface.getSuperInterfaces()); scan(CtRole.TYPE_PARAMETER, intrface.getFormalCtTypeParameters()); scan(CtRole.TYPE_MEMBER, intrface.getTypeMembers()); + scan(CtRole.PERMITTED_TYPE, intrface.getPermittedTypes()); scan(CtRole.COMMENT, intrface.getComments()); exit(intrface); } diff --git a/src/main/java/spoon/reflect/visitor/DefaultJavaPrettyPrinter.java b/src/main/java/spoon/reflect/visitor/DefaultJavaPrettyPrinter.java index 423680a552a..e5ee2751b8f 100644 --- a/src/main/java/spoon/reflect/visitor/DefaultJavaPrettyPrinter.java +++ b/src/main/java/spoon/reflect/visitor/DefaultJavaPrettyPrinter.java @@ -651,6 +651,7 @@ public void visitCtClass(CtClass ctClass) { elementPrinterHelper.writeExtendsClause(ctClass); elementPrinterHelper.writeImplementsClause(ctClass); } + elementPrinterHelper.printPermits(ctClass); printer.writeSpace().writeSeparator("{").incTab(); elementPrinterHelper.writeElementList(ctClass.getTypeMembers()); getPrinterHelper().adjustEndPosition(ctClass); @@ -1340,6 +1341,7 @@ public void visitCtInterface(CtInterface intrface) { "extends", false, null, false, true, ",", true, false, null, ref -> scan(ref)); } + elementPrinterHelper.printPermits(intrface); context.pushCurrentThis(intrface); printer.writeSpace().writeSeparator("{").incTab(); // Content diff --git a/src/main/java/spoon/reflect/visitor/ElementPrinterHelper.java b/src/main/java/spoon/reflect/visitor/ElementPrinterHelper.java index 4d2f75c42d6..17bc05386d4 100644 --- a/src/main/java/spoon/reflect/visitor/ElementPrinterHelper.java +++ b/src/main/java/spoon/reflect/visitor/ElementPrinterHelper.java @@ -21,8 +21,11 @@ import spoon.reflect.declaration.CtElement; import spoon.reflect.declaration.CtExecutable; import spoon.reflect.declaration.CtFormalTypeDeclarer; +import spoon.reflect.declaration.CtImport; +import spoon.reflect.declaration.CtMethod; import spoon.reflect.declaration.CtModifiable; import spoon.reflect.declaration.CtNamedElement; +import spoon.reflect.declaration.CtSealable; import spoon.reflect.declaration.CtType; import spoon.reflect.declaration.CtTypeMember; import spoon.reflect.declaration.CtTypeParameter; @@ -32,19 +35,16 @@ import spoon.reflect.reference.CtActualTypeContainer; import spoon.reflect.reference.CtExecutableReference; import spoon.reflect.reference.CtFieldReference; -import spoon.reflect.declaration.CtImport; -import spoon.reflect.declaration.CtMethod; import spoon.reflect.reference.CtPackageReference; -import spoon.reflect.reference.CtTypeReference; import spoon.reflect.reference.CtTypeMemberWildcardImportReference; -import spoon.reflect.visitor.printer.CommentOffset; +import spoon.reflect.reference.CtTypeReference; import spoon.reflect.visitor.PrintingContext.Writable; +import spoon.reflect.visitor.printer.CommentOffset; +import spoon.support.reflect.CtExtendedModifier; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; -import spoon.support.reflect.CtExtendedModifier; - import java.util.Collections; import java.util.HashSet; import java.util.List; @@ -547,4 +547,22 @@ public void printList(Iterable iterable, } } } + + /** + * Prints the {@code permits} keyword followed by the permitted + * types of a {@link CtSealable}. + *

+ * If the given sealed type does not have any + * explicit permitted types, nothing is printed. + * + * @param sealable the sealed type to print the permitted types for. + */ + protected void printPermits(CtSealable sealable) { + if (sealable.getPermittedTypes().isEmpty() || sealable.getPermittedTypes().stream().allMatch(CtElement::isImplicit)) { + return; + } + printer.writeln().incTab().writeKeyword("permits").writeSpace(); + printList(sealable.getPermittedTypes(), null, false, null, false, false, ",", true, false, null, prettyPrinter::scan); + printer.decTab(); + } } diff --git a/src/main/java/spoon/reflect/visitor/ForceFullyQualifiedProcessor.java b/src/main/java/spoon/reflect/visitor/ForceFullyQualifiedProcessor.java index 996a9a7f505..d16199b5faf 100644 --- a/src/main/java/spoon/reflect/visitor/ForceFullyQualifiedProcessor.java +++ b/src/main/java/spoon/reflect/visitor/ForceFullyQualifiedProcessor.java @@ -54,6 +54,10 @@ protected void handleTypeReference(CtTypeReference reference, LexicalScope na //do not use FQ names for that return; } + if (role == CtRole.PERMITTED_TYPE) { + // implicit permitted types are present in the same CU and don't need to be imported + return; + } //force fully qualified name reference.setImplicit(false); reference.setSimplyQualified(false); diff --git a/src/main/java/spoon/support/compiler/jdt/JDTTreeBuilderHelper.java b/src/main/java/spoon/support/compiler/jdt/JDTTreeBuilderHelper.java index 00f9f88b949..221935da62a 100644 --- a/src/main/java/spoon/support/compiler/jdt/JDTTreeBuilderHelper.java +++ b/src/main/java/spoon/support/compiler/jdt/JDTTreeBuilderHelper.java @@ -55,6 +55,7 @@ import spoon.reflect.declaration.CtPackageExport; import spoon.reflect.declaration.CtParameter; import spoon.reflect.declaration.CtProvidedService; +import spoon.reflect.declaration.CtSealable; import spoon.reflect.declaration.CtType; import spoon.reflect.declaration.CtUsedService; import spoon.reflect.declaration.CtVariable; @@ -76,6 +77,7 @@ import java.util.List; import java.util.Set; import java.util.StringJoiner; +import java.util.function.Consumer; import static spoon.support.compiler.jdt.JDTTreeBuilderQuery.getModifiers; import static spoon.support.compiler.jdt.JDTTreeBuilderQuery.isLhsAssignment; @@ -729,6 +731,26 @@ CtType createType(TypeDeclaration typeDeclaration) { type.addSuperInterface(superInterface); } } + Consumer> addPermittedType; + if (type instanceof CtSealable) { + addPermittedType = ((CtSealable) type)::addPermittedType; + } else { + addPermittedType = ref -> { + throw new SpoonException("Tried to add permitted type to " + type); + }; + } + if (typeDeclaration.permittedTypes != null) { + for (TypeReference permittedType : typeDeclaration.permittedTypes) { + CtTypeReference reference = jdtTreeBuilder.references.buildTypeReference(permittedType, typeDeclaration.scope); + addPermittedType.accept(reference); + } + } else if (typeDeclaration.binding != null && typeDeclaration.binding.permittedTypes != null) { + for (ReferenceBinding permittedType : typeDeclaration.binding.permittedTypes) { + CtTypeReference reference = jdtTreeBuilder.references.getTypeReference(permittedType); + reference.setImplicit(true); + addPermittedType.accept(reference); + } + } if (type instanceof CtClass && typeDeclaration.superclass != null) { ((CtClass) type).setSuperclass(jdtTreeBuilder.references.buildTypeReference(typeDeclaration.superclass, typeDeclaration.scope)); diff --git a/src/main/java/spoon/support/compiler/jdt/JDTTreeBuilderQuery.java b/src/main/java/spoon/support/compiler/jdt/JDTTreeBuilderQuery.java index c8ceb085677..10197649430 100644 --- a/src/main/java/spoon/support/compiler/jdt/JDTTreeBuilderQuery.java +++ b/src/main/java/spoon/support/compiler/jdt/JDTTreeBuilderQuery.java @@ -18,6 +18,7 @@ import org.eclipse.jdt.internal.compiler.ast.TypeDeclaration; import org.eclipse.jdt.internal.compiler.classfmt.ClassFileConstants; import org.eclipse.jdt.internal.compiler.lookup.AnnotationBinding; +import org.eclipse.jdt.internal.compiler.lookup.ExtraCompilerModifiers; import org.eclipse.jdt.internal.compiler.lookup.FieldBinding; import org.eclipse.jdt.internal.compiler.lookup.ProblemFieldBinding; import org.eclipse.jdt.internal.compiler.lookup.ReferenceBinding; @@ -28,6 +29,7 @@ import spoon.reflect.declaration.ModifierKind; import spoon.support.reflect.CtExtendedModifier; +import java.util.Collections; import java.util.HashSet; import java.util.Set; @@ -364,6 +366,13 @@ static Set getModifiers(int modifier, boolean implicit, Set< if ((modifier & ClassFileConstants.AccNative) != 0) { modifiers.add(new CtExtendedModifier(ModifierKind.NATIVE, implicit)); } + // AccSealed == AccPatternVariable == AccOverriding, so checking context is needed + // AccNonSealed == AccIsDefaultConstructor == AccBlankFinal, so checking context is needed + if ((modifier & ExtraCompilerModifiers.AccSealed) != 0 && intersect(target, ModifierTarget.TYPE)) { + modifiers.add(new CtExtendedModifier(ModifierKind.SEALED, implicit)); + } else if ((modifier & ExtraCompilerModifiers.AccNonSealed) != 0 && intersect(target, ModifierTarget.TYPE)) { + modifiers.add(new CtExtendedModifier(ModifierKind.NON_SEALED, implicit)); + } return modifiers; } @@ -378,4 +387,12 @@ static Set getModifiers(int modifier, boolean implicit, Set< static Set getModifiers(int modifier, boolean implicit, ModifierTarget target) { return getModifiers(modifier, implicit, target.asSingleton()); } + + /** + * Returns {@code true} if the given sets intersect, that means the intersection + * of {@code first} and {@code second} is non-empty. + */ + private static boolean intersect(Set first, Set second) { + return !Collections.disjoint(first, second); + } } diff --git a/src/main/java/spoon/support/compiler/jdt/ModifierTarget.java b/src/main/java/spoon/support/compiler/jdt/ModifierTarget.java index 239f2921f74..89ee7ad7346 100644 --- a/src/main/java/spoon/support/compiler/jdt/ModifierTarget.java +++ b/src/main/java/spoon/support/compiler/jdt/ModifierTarget.java @@ -68,6 +68,8 @@ enum ModifierTarget { ModifierKind.PRIVATE, ModifierKind.ABSTRACT, ModifierKind.STATIC, + ModifierKind.SEALED, + ModifierKind.NON_SEALED, ModifierKind.FINAL, ModifierKind.STRICTFP ), @@ -81,6 +83,8 @@ enum ModifierTarget { ModifierKind.PRIVATE, ModifierKind.ABSTRACT, ModifierKind.STATIC, + ModifierKind.SEALED, + ModifierKind.NON_SEALED, ModifierKind.STRICTFP ), /** diff --git a/src/main/java/spoon/support/compiler/jdt/ParentExiter.java b/src/main/java/spoon/support/compiler/jdt/ParentExiter.java index a430d819495..217b9bdbcb0 100644 --- a/src/main/java/spoon/support/compiler/jdt/ParentExiter.java +++ b/src/main/java/spoon/support/compiler/jdt/ParentExiter.java @@ -421,6 +421,8 @@ public void visitCtBinaryOperator(CtBinaryOperator operator) { if (child instanceof CtLocalVariable && operator.getKind() == INSTANCEOF && operator.getLeftHandOperand() != null) { CtTypePattern typePattern = child.getFactory().Core().createTypePattern(); typePattern.setVariable((CtLocalVariable) child); + // as we create the type pattern just here, we need to set its source position - which is luckily the same + typePattern.setPosition(child.getPosition()); child = typePattern; // replace the local variable with a pattern (which is a CtExpression) } if (child instanceof CtExpression) { @@ -518,7 +520,9 @@ public void visitCtCatchVariable(CtCatchVariable e) { @Override public void visitCtClass(CtClass ctClass) { if (child instanceof CtConstructor) { - ctClass.addConstructor((CtConstructor) child); + CtConstructor constructor = (CtConstructor) child; + ctClass.addConstructor(constructor); + fixJdtEnumConstructorSuperCall(ctClass, constructor); } if (child instanceof CtAnonymousExecutable) { ctClass.addAnonymousExecutable((CtAnonymousExecutable) child); @@ -526,6 +530,31 @@ public void visitCtClass(CtClass ctClass) { super.visitCtClass(ctClass); } + private void fixJdtEnumConstructorSuperCall(CtClass ctClass, CtConstructor constructor) { + // For some reason JDT inserts a `super()` call in implicit enum constructors. + // Explicit super calls are forbidden as java.lang.Enum subclasses are permitted by the JLS to delegate + // to the Enum constructor in whatever way they like. + // The constructor is implicit so this isn't *technically* illegal, but it doesn't really make much sense + // as explicit constructors can never contain such a call. Additionally, the Enum class from the standard + // library has a "String, int" constructor, rendering the parameterless supercall semantically invalid. + // We just remove the call to make it a bit more consistent. + // See https://github.com/INRIA/spoon/issues/4758 for more details. + if (!child.isImplicit() || !ctClass.isEnum() || !constructor.getParameters().isEmpty()) { + return; + } + if (constructor.getBody().getStatements().isEmpty()) { + return; + } + if (!(constructor.getBody().getStatement(0) instanceof CtInvocation)) { + return; + } + + CtInvocation superCall = constructor.getBody().getStatement(0); + if (superCall.getExecutable().getSimpleName().equals("")) { + constructor.getBody().removeStatement(superCall); + } + } + @Override public void visitCtTypeParameter(CtTypeParameter typeParameter) { if (childJDT instanceof TypeReference && child instanceof CtTypeAccess) { diff --git a/src/main/java/spoon/support/compiler/jdt/PositionBuilder.java b/src/main/java/spoon/support/compiler/jdt/PositionBuilder.java index bdd1dc2fbc1..bf033821662 100644 --- a/src/main/java/spoon/support/compiler/jdt/PositionBuilder.java +++ b/src/main/java/spoon/support/compiler/jdt/PositionBuilder.java @@ -420,9 +420,17 @@ SourcePosition buildPositionCtElement(CtElement e, ASTNode node) { AllocationExpression allocationExpression = (AllocationExpression) node; if (allocationExpression.enumConstant != null) { FieldDeclaration fieldDeclaration = allocationExpression.enumConstant; - //1) skip comments + + //1) skip the annotations + Annotation[] annotations = allocationExpression.enumConstant.annotations; + if (annotations != null && annotations.length > 0) { + Annotation lastAnnotation = annotations[annotations.length - 1]; + sourceStart = findNextNonWhitespace(contents, sourceEnd, lastAnnotation.sourceEnd); + } + + //2) skip comments sourceStart = findNextNonWhitespace(contents, sourceEnd, sourceStart); - //2) move to beginning of enum construction + //3) move to beginning of enum construction sourceStart += fieldDeclaration.name.length; } } else if (node instanceof CaseStatement) { diff --git a/src/main/java/spoon/support/reflect/declaration/CtClassImpl.java b/src/main/java/spoon/support/reflect/declaration/CtClassImpl.java index c5825bb5369..3f946dbe526 100644 --- a/src/main/java/spoon/support/reflect/declaration/CtClassImpl.java +++ b/src/main/java/spoon/support/reflect/declaration/CtClassImpl.java @@ -19,6 +19,7 @@ import spoon.reflect.declaration.CtExecutable; import spoon.reflect.declaration.CtType; import spoon.reflect.declaration.CtTypeMember; +import spoon.reflect.path.CtRole; import spoon.reflect.reference.CtExecutableReference; import spoon.reflect.reference.CtTypeReference; import spoon.reflect.visitor.CtVisitor; @@ -35,6 +36,7 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.LinkedHashSet; import java.util.List; import java.util.Set; @@ -53,6 +55,9 @@ public class CtClassImpl extends CtTypeImpl implements CtClass { @MetamodelPropertyField(role = SUPER_TYPE) CtTypeReference superClass; + @MetamodelPropertyField(role = CtRole.PERMITTED_TYPE) + Set> permittedTypes = emptySet(); + @Override public void accept(CtVisitor v) { v.visitCtClass(this); @@ -289,4 +294,44 @@ public Collection> getAllExecutables() { } return l; } + + @Override + public Set> getPermittedTypes() { + return Collections.unmodifiableSet(permittedTypes); + } + + @Override + public CtClass setPermittedTypes(Collection> permittedTypes) { + Collection> types = permittedTypes != null ? permittedTypes : CtElementImpl.emptySet(); + getFactory().getEnvironment().getModelChangeListener().onSetDeleteAll(this, CtRole.PERMITTED_TYPE, this.permittedTypes, new LinkedHashSet<>(this.permittedTypes)); + this.permittedTypes.clear(); + for (CtTypeReference type : types) { + addPermittedType(type); + } + return this; + } + + @Override + public CtClass addPermittedType(CtTypeReference type) { + if (type == null) { + return this; + } + if (this.permittedTypes == CtElementImpl.>emptySet()) { + this.permittedTypes = new LinkedHashSet<>(); + } + type.setParent(this); + getFactory().getEnvironment().getModelChangeListener().onSetAdd(this, CtRole.PERMITTED_TYPE, this.permittedTypes, type); + this.permittedTypes.add(type); + return this; + } + + @Override + public CtClass removePermittedType(CtTypeReference type) { + if (this.permittedTypes == CtElementImpl.>emptySet()) { + return this; + } + getFactory().getEnvironment().getModelChangeListener().onSetDelete(this, CtRole.PERMITTED_TYPE, this.permittedTypes, type); + this.permittedTypes.remove(type); + return this; + } } diff --git a/src/main/java/spoon/support/reflect/declaration/CtEnumImpl.java b/src/main/java/spoon/support/reflect/declaration/CtEnumImpl.java index c6d10e3c55c..08133d51ce1 100644 --- a/src/main/java/spoon/support/reflect/declaration/CtEnumImpl.java +++ b/src/main/java/spoon/support/reflect/declaration/CtEnumImpl.java @@ -8,6 +8,7 @@ package spoon.support.reflect.declaration; import spoon.reflect.annotations.MetamodelPropertyField; +import spoon.reflect.code.CtNewClass; import spoon.reflect.declaration.CtEnum; import spoon.reflect.declaration.CtEnumValue; import spoon.reflect.declaration.CtField; @@ -23,7 +24,9 @@ import spoon.support.util.SignatureBasedSortedSet; import java.util.ArrayList; +import java.util.Collection; import java.util.Collections; +import java.util.LinkedHashSet; import java.util.List; import java.util.Set; @@ -143,6 +146,36 @@ public CtEnum clone() { return (CtEnum) super.clone(); } + @Override + @DerivedProperty + public Set> getPermittedTypes() { + LinkedHashSet> refs = new LinkedHashSet<>(); + for (CtEnumValue value : enumValues) { + if (value.getDefaultExpression() instanceof CtNewClass) { + refs.add(((CtNewClass) value.getDefaultExpression()).getAnonymousClass().getReference()); + } + } + return Collections.unmodifiableSet(refs); + } + + @Override + @UnsettableProperty + public CtEnum setPermittedTypes(Collection> permittedTypes) { + return this; + } + + @Override + @UnsettableProperty + public CtEnum addPermittedType(CtTypeReference type) { + return this; + } + + @Override + @UnsettableProperty + public CtEnum removePermittedType(CtTypeReference type) { + return this; + } + @Override @DerivedProperty public CtTypeReference getSuperclass() { diff --git a/src/main/java/spoon/support/reflect/declaration/CtInterfaceImpl.java b/src/main/java/spoon/support/reflect/declaration/CtInterfaceImpl.java index 13bf3313ce1..11f6b912523 100644 --- a/src/main/java/spoon/support/reflect/declaration/CtInterfaceImpl.java +++ b/src/main/java/spoon/support/reflect/declaration/CtInterfaceImpl.java @@ -7,6 +7,7 @@ */ package spoon.support.reflect.declaration; +import spoon.reflect.annotations.MetamodelPropertyField; import spoon.reflect.code.CtCodeElement; import spoon.reflect.code.CtStatement; import spoon.reflect.code.CtStatementList; @@ -14,6 +15,7 @@ import spoon.reflect.declaration.CtType; import spoon.reflect.declaration.ModifierKind; import spoon.reflect.declaration.ParentNotInitializedException; +import spoon.reflect.path.CtRole; import spoon.reflect.reference.CtExecutableReference; import spoon.reflect.reference.CtTypeReference; import spoon.reflect.visitor.CtVisitor; @@ -27,12 +29,16 @@ import java.util.Collections; import java.util.EnumSet; import java.util.HashSet; +import java.util.LinkedHashSet; import java.util.List; import java.util.Set; public class CtInterfaceImpl extends CtTypeImpl implements CtInterface { private static final long serialVersionUID = 1L; + @MetamodelPropertyField(role = CtRole.PERMITTED_TYPE) + Set> permittedTypes = emptySet(); + @Override public void accept(CtVisitor visitor) { visitor.visitCtInterface(this); @@ -114,6 +120,46 @@ public String getLabel() { return null; } + @Override + public Set> getPermittedTypes() { + return Collections.unmodifiableSet(permittedTypes); + } + + @Override + public CtInterface setPermittedTypes(Collection> permittedTypes) { + Collection> types = permittedTypes != null ? permittedTypes : CtElementImpl.emptySet(); + getFactory().getEnvironment().getModelChangeListener().onSetDeleteAll(this, CtRole.PERMITTED_TYPE, this.permittedTypes, new LinkedHashSet<>(this.permittedTypes)); + this.permittedTypes.clear(); + for (CtTypeReference type : types) { + addPermittedType(type); + } + return this; + } + + @Override + public CtInterface addPermittedType(CtTypeReference type) { + if (type == null) { + return this; + } + if (this.permittedTypes == CtElementImpl.>emptySet()) { + this.permittedTypes = new LinkedHashSet<>(); + } + type.setParent(this); + getFactory().getEnvironment().getModelChangeListener().onSetAdd(this, CtRole.PERMITTED_TYPE, this.permittedTypes, type); + this.permittedTypes.add(type); + return this; + } + + @Override + public CtInterface removePermittedType(CtTypeReference type) { + if (this.permittedTypes == CtElementImpl.>emptySet()) { + return this; + } + getFactory().getEnvironment().getModelChangeListener().onSetDelete(this, CtRole.PERMITTED_TYPE, this.permittedTypes, type); + this.permittedTypes.remove(type); + return this; + } + @Override public > C addNestedType(CtType nestedType) { super.addNestedType(nestedType); diff --git a/src/main/java/spoon/support/reflect/declaration/CtRecordImpl.java b/src/main/java/spoon/support/reflect/declaration/CtRecordImpl.java index 883b29cdc4f..9b3abafccec 100644 --- a/src/main/java/spoon/support/reflect/declaration/CtRecordImpl.java +++ b/src/main/java/spoon/support/reflect/declaration/CtRecordImpl.java @@ -12,6 +12,7 @@ import java.lang.annotation.Target; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.LinkedHashSet; @@ -241,6 +242,29 @@ public E setParent(CtElement parent) { return super.setParent(parent); } + @Override + public Set> getPermittedTypes() { + return Set.of(); + } + + @Override + @UnsettableProperty + public CtRecord setPermittedTypes(Collection> permittedTypes) { + return this; + } + + @Override + @UnsettableProperty + public CtRecord addPermittedType(CtTypeReference type) { + return this; + } + + @Override + @UnsettableProperty + public CtRecord removePermittedType(CtTypeReference type) { + return this; + } + @Override public CtRecord clone() { return (CtRecord) super.clone(); diff --git a/src/main/java/spoon/support/sniper/internal/ElementSourceFragment.java b/src/main/java/spoon/support/sniper/internal/ElementSourceFragment.java index 08defbff5c7..b59a267e448 100644 --- a/src/main/java/spoon/support/sniper/internal/ElementSourceFragment.java +++ b/src/main/java/spoon/support/sniper/internal/ElementSourceFragment.java @@ -369,7 +369,7 @@ private CMP compare(ElementSourceFragment other) { return CMP.OTHER_IS_PARENT; } //the fragments overlap - it is not allowed - throw new SpoonException("Cannot compare this: [" + getStart() + ", " + getEnd() + "] with other: [\"" + other.getStart() + "\", \"" + other.getEnd() + "\"]"); + throw new SpoonException("Cannot compare this: [" + getStart() + ", " + getEnd() + "] with other: [" + other.getStart() + ", " + other.getEnd() + "]"); } /** diff --git a/src/main/java/spoon/support/visitor/clone/CloneVisitor.java b/src/main/java/spoon/support/visitor/clone/CloneVisitor.java index 06c21cd73c9..ee82bc1a210 100644 --- a/src/main/java/spoon/support/visitor/clone/CloneVisitor.java +++ b/src/main/java/spoon/support/visitor/clone/CloneVisitor.java @@ -198,6 +198,7 @@ public void visitCtClass(final spoon.reflect.declaration.CtClass ctClass) aCtClass.setSuperInterfaces(this.cloneHelper.clone(ctClass.getSuperInterfaces())); aCtClass.setFormalCtTypeParameters(this.cloneHelper.clone(ctClass.getFormalCtTypeParameters())); aCtClass.setTypeMembers(this.cloneHelper.clone(ctClass.getTypeMembers())); + aCtClass.setPermittedTypes(this.cloneHelper.clone(ctClass.getPermittedTypes())); aCtClass.setComments(this.cloneHelper.clone(ctClass.getComments())); this.cloneHelper.tailor(ctClass, aCtClass); this.other = aCtClass; @@ -404,6 +405,7 @@ public void visitCtInterface(final spoon.reflect.declaration.CtInterface aCtInterface.setSuperInterfaces(this.cloneHelper.clone(intrface.getSuperInterfaces())); aCtInterface.setFormalCtTypeParameters(this.cloneHelper.clone(intrface.getFormalCtTypeParameters())); aCtInterface.setTypeMembers(this.cloneHelper.clone(intrface.getTypeMembers())); + aCtInterface.setPermittedTypes(this.cloneHelper.clone(intrface.getPermittedTypes())); aCtInterface.setComments(this.cloneHelper.clone(intrface.getComments())); this.cloneHelper.tailor(intrface, aCtInterface); this.other = aCtInterface; diff --git a/src/main/java/spoon/support/visitor/java/JavaReflectionVisitorImpl.java b/src/main/java/spoon/support/visitor/java/JavaReflectionVisitorImpl.java index 6807b2d655b..b440768d989 100644 --- a/src/main/java/spoon/support/visitor/java/JavaReflectionVisitorImpl.java +++ b/src/main/java/spoon/support/visitor/java/JavaReflectionVisitorImpl.java @@ -116,6 +116,7 @@ public void visitClass(Class clazz) { } catch (NoClassDefFoundError ignore) { // partial classpath } + scanPermittedTypes(clazz); } protected final void visitType(Class aClass) { @@ -186,6 +187,7 @@ public void visitInterface(Class clazz) { } catch (NoClassDefFoundError ignore) { // partial classpath } + scanPermittedTypes(clazz); } @Override @@ -260,6 +262,7 @@ public void visitEnum(Class clazz) { } catch (NoClassDefFoundError ignore) { // partial classpath } + scanPermittedTypes(clazz); } @Override @@ -619,4 +622,17 @@ public void visitRecordComponent(AnnotatedElement recordComponent) { } + private void scanPermittedTypes(Class clazz) { + Class[] permittedSubclasses = MethodHandleUtils.getPermittedSubclasses(clazz); + if (permittedSubclasses == null) { + return; + } + for (Class subclass : permittedSubclasses) { + try { + visitTypeReference(CtRole.PERMITTED_TYPE, subclass); + } catch (NoClassDefFoundError ignore) { + // partial classpath + } + } + } } diff --git a/src/main/java/spoon/support/visitor/java/MethodHandleUtils.java b/src/main/java/spoon/support/visitor/java/MethodHandleUtils.java index 3b44e4f02e2..3f4b0650938 100644 --- a/src/main/java/spoon/support/visitor/java/MethodHandleUtils.java +++ b/src/main/java/spoon/support/visitor/java/MethodHandleUtils.java @@ -21,6 +21,7 @@ * This defines multiple utility methods for working with method handles. These methods are all calls to future jdk methods and maybe not available on jdk8. * All errors from these virtual calls are transformed to null or false results. */ +@SuppressWarnings({"JavaLangInvokeHandleSignature", "ReturnOfNull"}) class MethodHandleUtils { private MethodHandleUtils() { // no instance @@ -32,6 +33,8 @@ private MethodHandleUtils() { private static MethodHandle lookupRecordComponentName = lookupRecordComponentName(); private static MethodHandle lookupRecordComponentType = lookupRecordComponentType(); + private static MethodHandle lookupPermittedSubclasses = lookupPermittedSubclasses(); + /** * Checks if the given class is a record. * @param clazz the class to check @@ -113,6 +116,14 @@ public static Type getRecordComponentType(AnnotatedElement component) { } } + public static Class[] getPermittedSubclasses(Class clazz) { + try { + return (Class[]) lookupPermittedSubclasses.invoke(clazz); + } catch (Throwable e) { + return null; + } + } + private static MethodHandle lookupRecord() { try { @@ -144,4 +155,12 @@ private static MethodHandle lookupRecordComponentName() { return null; } } + + private static MethodHandle lookupPermittedSubclasses() { + try { + return MethodHandles.lookup().findVirtual(Class.class, "getPermittedSubclasses", MethodType.methodType(Class[].class)); + } catch (Throwable e) { + return null; + } + } } diff --git a/src/main/java/spoon/support/visitor/java/internal/TypeRuntimeBuilderContext.java b/src/main/java/spoon/support/visitor/java/internal/TypeRuntimeBuilderContext.java index 9f6a14b466e..5faf93249f0 100644 --- a/src/main/java/spoon/support/visitor/java/internal/TypeRuntimeBuilderContext.java +++ b/src/main/java/spoon/support/visitor/java/internal/TypeRuntimeBuilderContext.java @@ -11,8 +11,10 @@ import spoon.reflect.declaration.CtField; import spoon.reflect.declaration.CtMethod; import spoon.reflect.declaration.CtPackage; +import spoon.reflect.declaration.CtSealable; import spoon.reflect.declaration.CtType; import spoon.reflect.declaration.CtTypeParameter; +import spoon.reflect.declaration.ModifierKind; import spoon.reflect.path.CtRole; import spoon.reflect.reference.CtTypeReference; @@ -67,19 +69,40 @@ public void addFormalType(CtTypeParameter parameterRef) { @Override public void addTypeReference(CtRole role, CtTypeReference typeReference) { + // if the type reference is known, we can use it. But we don't want to recursively build models for + // type references, so CtTypeReference#getTypeDeclaration() does not work + CtType declaringType = typeReference.getFactory().Type().get(typeReference.getQualifiedName()); + boolean finalOrSealed = type.isFinal() || type.hasModifier(ModifierKind.SEALED); switch (role) { case INTERFACE: + if (declaringType != null && declaringType.isShadow()) { + if (!finalOrSealed && declaringType.hasModifier(ModifierKind.SEALED)) { + type.addModifier(ModifierKind.NON_SEALED); + } + } type.addSuperInterface(typeReference); return; case SUPER_TYPE: + if (declaringType != null && declaringType.isShadow()) { + if (!finalOrSealed && declaringType.hasModifier(ModifierKind.SEALED)) { + type.addModifier(ModifierKind.NON_SEALED); + } + } if (type instanceof CtTypeParameter) { ((CtTypeParameter) this.type).setSuperclass(typeReference); } else { type.setSuperclass(typeReference); } return; + case PERMITTED_TYPE: + if (type instanceof CtSealable) { + type.addModifier(ModifierKind.SEALED); + ((CtSealable) type).addPermittedType(typeReference); + } + return; + default: + throw new UnsupportedOperationException(); } - throw new UnsupportedOperationException(); } @Override diff --git a/src/main/java/spoon/support/visitor/replace/ReplacementVisitor.java b/src/main/java/spoon/support/visitor/replace/ReplacementVisitor.java index c3abd9c1913..83b007f06f0 100644 --- a/src/main/java/spoon/support/visitor/replace/ReplacementVisitor.java +++ b/src/main/java/spoon/support/visitor/replace/ReplacementVisitor.java @@ -992,6 +992,20 @@ public void set(spoon.reflect.code.CtExpression replace) { } } + // auto-generated, see spoon.generating.ReplacementVisitorGenerator + static class CtSealablePermittedTypesReplaceListener implements spoon.support.visitor.replace.ReplaceSetListener { + private final spoon.reflect.declaration.CtSealable element; + + CtSealablePermittedTypesReplaceListener(spoon.reflect.declaration.CtSealable element) { + this.element = element; + } + + @java.lang.Override + public void set(java.util.Set replace) { + this.element.setPermittedTypes(replace); + } + } + // auto-generated, see spoon.generating.ReplacementVisitorGenerator static class CtFormalTypeDeclarerFormalCtTypeParametersReplaceListener implements spoon.support.visitor.replace.ReplaceListListener { private final spoon.reflect.declaration.CtFormalTypeDeclarer element; @@ -1613,6 +1627,7 @@ public void visitCtClass(final spoon.reflect.declaration.CtClass ctClass) replaceInSetIfExist(ctClass.getSuperInterfaces(), new spoon.support.visitor.replace.ReplacementVisitor.CtTypeInformationSuperInterfacesReplaceListener(ctClass)); replaceInListIfExist(ctClass.getFormalCtTypeParameters(), new spoon.support.visitor.replace.ReplacementVisitor.CtFormalTypeDeclarerFormalCtTypeParametersReplaceListener(ctClass)); replaceInListIfExist(ctClass.getTypeMembers(), new spoon.support.visitor.replace.ReplacementVisitor.CtTypeTypeMembersReplaceListener(ctClass)); + replaceInSetIfExist(ctClass.getPermittedTypes(), new spoon.support.visitor.replace.ReplacementVisitor.CtSealablePermittedTypesReplaceListener(ctClass)); replaceInListIfExist(ctClass.getComments(), new spoon.support.visitor.replace.ReplacementVisitor.CtElementCommentsReplaceListener(ctClass)); } @@ -1768,6 +1783,7 @@ public void visitCtInterface(final spoon.reflect.declaration.CtInterface replaceInSetIfExist(intrface.getSuperInterfaces(), new spoon.support.visitor.replace.ReplacementVisitor.CtTypeInformationSuperInterfacesReplaceListener(intrface)); replaceInListIfExist(intrface.getFormalCtTypeParameters(), new spoon.support.visitor.replace.ReplacementVisitor.CtFormalTypeDeclarerFormalCtTypeParametersReplaceListener(intrface)); replaceInListIfExist(intrface.getTypeMembers(), new spoon.support.visitor.replace.ReplacementVisitor.CtTypeTypeMembersReplaceListener(intrface)); + replaceInSetIfExist(intrface.getPermittedTypes(), new spoon.support.visitor.replace.ReplacementVisitor.CtSealablePermittedTypesReplaceListener(intrface)); replaceInListIfExist(intrface.getComments(), new spoon.support.visitor.replace.ReplacementVisitor.CtElementCommentsReplaceListener(intrface)); } diff --git a/src/test/java/spoon/generating/CloneVisitorGenerator.java b/src/test/java/spoon/generating/CloneVisitorGenerator.java index b0c0b6be8f3..b38a805a838 100644 --- a/src/test/java/spoon/generating/CloneVisitorGenerator.java +++ b/src/test/java/spoon/generating/CloneVisitorGenerator.java @@ -213,7 +213,7 @@ private CtInvocation createFactoryInvocation(CtVariableAccess element "spoon.support.reflect.declaration.CtModifiableImpl", "spoon.support.reflect.declaration.CtMultiTypedElementImpl", // "spoon.support.reflect.declaration.CtTypeMemberImpl", "spoon.support.reflect.code.CtRHSReceiverImpl", "spoon.support.reflect.code.CtResourceImpl", "spoon.support.reflect.declaration.CtShadowableImpl", "spoon.support.reflect.code.CtBodyHolderImpl", "spoon.support.reflect.declaration.CtModuleDirectiveImpl", - "spoon.support.reflect.code.CtPatternImpl"); + "spoon.support.reflect.code.CtPatternImpl", "spoon.support.reflect.declaration.CtSealableImpl"); private final List excludesFields = Arrays.asList("factory", "elementValues", "target", "rootFragment", "originalSourceCode", "myPartialSourcePosition"); private final Set collectionClasses = new HashSet<>(Arrays.asList( List.class.getName(), Collection.class.getName(), Set.class.getName(), diff --git a/src/test/java/spoon/reflect/factory/PackageFactoryTest.java b/src/test/java/spoon/reflect/factory/PackageFactoryTest.java new file mode 100644 index 00000000000..13cb1897fa1 --- /dev/null +++ b/src/test/java/spoon/reflect/factory/PackageFactoryTest.java @@ -0,0 +1,31 @@ +package spoon.reflect.factory; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.sameInstance; +import static org.hamcrest.MatcherAssert.assertThat; + +import spoon.Launcher; +import spoon.reflect.declaration.CtPackage; +import spoon.test.GitHubIssue; + +class PackageFactoryTest { + + @GitHubIssue(issueNumber = 4764, fixed = true) + void getOrCreate_returnsNestedPackageStructure_whenQualifiedNameRepeatsSimpleName() { + // contract: A qualified name that is simply repetitions of a single simple name results in the expected + // nested package structure + + PackageFactory factory = new Launcher().getFactory().Package(); + String topLevelPackageName = "spoon"; + String nestedPackageName = topLevelPackageName + "." + topLevelPackageName; + + CtPackage packageWithDuplicatedSimpleNames = factory.getOrCreate(nestedPackageName); + CtPackage topLevelPackage = factory.get(topLevelPackageName); + + assertThat(topLevelPackage.getQualifiedName(), equalTo(topLevelPackageName)); + assertThat(packageWithDuplicatedSimpleNames.getQualifiedName(), equalTo(nestedPackageName)); + + assertThat(topLevelPackage.getPackage(topLevelPackageName), sameInstance(packageWithDuplicatedSimpleNames)); + assertThat(packageWithDuplicatedSimpleNames.getParent(), sameInstance(topLevelPackage)); + } +} \ No newline at end of file diff --git a/src/test/java/spoon/reflect/visitor/DefaultJavaPrettyPrinterTest.java b/src/test/java/spoon/reflect/visitor/DefaultJavaPrettyPrinterTest.java index e5b509b90c8..80baa044624 100644 --- a/src/test/java/spoon/reflect/visitor/DefaultJavaPrettyPrinterTest.java +++ b/src/test/java/spoon/reflect/visitor/DefaultJavaPrettyPrinterTest.java @@ -2,7 +2,11 @@ import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.ArgumentsProvider; +import org.junit.jupiter.params.provider.ArgumentsSource; import org.junit.jupiter.params.provider.ValueSource; import spoon.Launcher; import spoon.reflect.CtModel; @@ -10,12 +14,17 @@ import spoon.reflect.code.CtExecutableReferenceExpression; import spoon.reflect.code.CtExpression; import spoon.reflect.code.CtStatement; +import spoon.reflect.declaration.CtClass; import spoon.reflect.declaration.CtCompilationUnit; import spoon.reflect.declaration.CtType; +import spoon.reflect.declaration.ModifierKind; import spoon.reflect.factory.Factory; import spoon.reflect.reference.CtTypeReference; import spoon.test.SpoonTestHelpers; +import java.util.List; +import java.util.stream.Stream; + import static org.hamcrest.CoreMatchers.allOf; import static org.hamcrest.CoreMatchers.anyOf; import static org.hamcrest.CoreMatchers.containsString; @@ -192,4 +201,49 @@ void testPrintMethodReferenceTypeParameters() { exeExpression.getExecutable().addActualTypeArgument(complexTypeReference); assertThat(exeExpression.toString(), containsRegexMatch("<(.*)Comparable<(.*)Integer, (.*)Comhyperbola<(.*)Integer>>>")); } + + @ParameterizedTest + @ArgumentsSource(SealedTypesProvider.class) + void testPrintSealedTypes(CtType sealedType, List explicitPermitted) { + // contract: sealed types are printed correctly + String printed = sealedType.toString(); + // the sealed keyword is always required + assertThat(printed, containsString("sealed")); + if (explicitPermitted.isEmpty()) { + // the permits keyword should only exist if explicit permitted types are printed + assertThat(printed, not(containsString("permits"))); + } else { + assertThat(printed, containsString("permits")); + for (String permitted : explicitPermitted) { + assertThat(printed, containsRegexMatch("\\s" + permitted)); + } + } + } + + @Test + void testPrintNonSealedTypes() { + // contract: the non-sealed modifier is printed + Launcher launcher = new Launcher(); + CtClass ctClass = launcher.getFactory().Class().create("MyClass"); + ctClass.addModifier(ModifierKind.NON_SEALED); + assertThat(ctClass.toString(), containsString("non-sealed ")); + } + + static class SealedTypesProvider implements ArgumentsProvider { + + @Override + public Stream provideArguments(ExtensionContext extensionContext) { + Launcher launcher = new Launcher(); + launcher.getEnvironment().setComplianceLevel(17); + launcher.addInputResource("src/test/resources/sealedclasses"); + launcher.buildModel(); + Factory factory = launcher.getFactory(); + return Stream.of( + Arguments.of(factory.Type().get("SealedClassWithPermits"), List.of("ExtendingClass", "OtherExtendingClass")), + Arguments.of(factory.Type().get("SealedInterfaceWithPermits"), List.of("ExtendingClass", "OtherExtendingClass")), + Arguments.of(factory.Type().get("SealedClassWithNestedSubclasses"), List.of()), // implicit + Arguments.of(factory.Type().get("SealedInterfaceWithNestedSubclasses"), List.of()) // implicit + ); + } + } } diff --git a/src/test/java/spoon/support/compiler/jdt/JDTTreeBuilderQueryTest.java b/src/test/java/spoon/support/compiler/jdt/JDTTreeBuilderQueryTest.java index ddc6daa8bb6..65216f8e728 100644 --- a/src/test/java/spoon/support/compiler/jdt/JDTTreeBuilderQueryTest.java +++ b/src/test/java/spoon/support/compiler/jdt/JDTTreeBuilderQueryTest.java @@ -66,6 +66,8 @@ static void setUp() { modifierMap.put(classFileConstant("AccSynchronized"), ModifierKind.SYNCHRONIZED); modifierMap.put(classFileConstant("AccNative"), ModifierKind.NATIVE); modifierMap.put(classFileConstant("AccStrictfp"), ModifierKind.STRICTFP); + modifierMap.put(extraCompilerModifier("AccSealed"), ModifierKind.SEALED); + modifierMap.put(extraCompilerModifier("AccNonSealed"), ModifierKind.NON_SEALED); List remaining = Arrays.stream(ModifierKind.values()) .filter(k -> !modifierMap.containsValue(k)) .collect(Collectors.toList()); @@ -97,6 +99,15 @@ private static Field classFileConstant(String name) { } } + private static Field extraCompilerModifier(String name) { + try { + return ExtraCompilerModifiers.class.getDeclaredField(name); + } catch (NoSuchFieldException e) { + fail(e); + return null; + } + } + @Test void testTargetsNotEmpty() { // contract: all ModifierKinds need to be an allowed kind of at least one ModifierTargets @@ -202,4 +213,4 @@ public Stream provideArguments(ExtensionContext extensionCo .map(Arguments::of); } } -} \ No newline at end of file +} diff --git a/src/test/java/spoon/support/visitor/java/JavaReflectionTreeBuilderTest.java b/src/test/java/spoon/support/visitor/java/JavaReflectionTreeBuilderTest.java index 1b160157ce6..a58772a3724 100644 --- a/src/test/java/spoon/support/visitor/java/JavaReflectionTreeBuilderTest.java +++ b/src/test/java/spoon/support/visitor/java/JavaReflectionTreeBuilderTest.java @@ -17,7 +17,10 @@ package spoon.support.visitor.java; import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.hasItem; +import static org.hamcrest.CoreMatchers.hasItems; import static org.hamcrest.CoreMatchers.instanceOf; +import static org.hamcrest.CoreMatchers.not; import static org.hamcrest.MatcherAssert.assertThat; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -91,6 +94,7 @@ import spoon.reflect.visitor.filter.TypeFilter; import spoon.support.compiler.FileSystemFile; import spoon.support.compiler.jdt.JDTSnippetCompiler; +import spoon.support.reflect.CtExtendedModifier; import spoon.support.reflect.code.CtAssignmentImpl; import spoon.support.reflect.code.CtConditionalImpl; import spoon.support.reflect.declaration.CtEnumValueImpl; @@ -773,6 +777,40 @@ void testShadowPackage() { assertEquals(ctPackage.getAnnotations().get(0).getAnnotationType().getQualifiedName(), "java.lang.Deprecated"); } + @Test + @EnabledForJreRange(min = JRE.JAVA_17) + void testShadowSealedTypes() throws ClassNotFoundException { + // contract: sealed/non-sealed types are in the shadow model + Factory factory = createFactory(); + // load a few ConstantDesc types + Class constantDesc = Class.forName("java.lang.constant.ConstantDesc"); // since Java 12, sealed since Java 17 + Class dynamicConstantDesc = Class.forName("java.lang.constant.DynamicConstantDesc"); // since Java 12 + Class enumDesc = Class.forName("java.lang.Enum$EnumDesc"); // since Java 12 + CtInterface ctConstantDesc = (CtInterface) factory.Type().get(constantDesc); + CtType ctDynamicConstantDesc = factory.Type().get(dynamicConstantDesc); + CtType ctEnumDesc = factory.Type().get(enumDesc); + CtType ctString = factory.Type().get(String.class); + + // make sure they are loaded correctly + assertNotNull(ctConstantDesc); + assertNotNull(ctDynamicConstantDesc); + assertNotNull(ctEnumDesc); + assertNotNull(ctString); + + // ConstDesc is sealed + assertThat(ctConstantDesc.getExtendedModifiers(), hasItem(CtExtendedModifier.explicit(ModifierKind.SEALED))); + // DynamicConstDesc and String are permitted types + assertThat(ctConstantDesc.getPermittedTypes(), hasItems(ctDynamicConstantDesc.getReference(), ctString.getReference())); + // EnumDesc extends DynamicConstantDesc, so it should not be added to the permitted types of ConstantDesc + assertThat(ctConstantDesc.getPermittedTypes(), not(hasItem(ctEnumDesc.getReference()))); + // DynamicConstDesc is non-sealed + assertThat(ctDynamicConstantDesc.getExtendedModifiers(), hasItem(CtExtendedModifier.explicit(ModifierKind.NON_SEALED))); + // EnumDesc extends DynamicConstDesc which is non-sealed, so it is not non-sealed itself + assertThat(ctEnumDesc.getModifiers(), not(hasItem(ModifierKind.NON_SEALED))); + // String is final and not sealed, so neither sealed nor non-sealed should be applied + assertThat(ctString.getModifiers(), not(hasItems(ModifierKind.SEALED, ModifierKind.NON_SEALED))); + } + @Test void testCyclicAnnotationScanning() { // contract: scanning annotations does not cause StackOverflowError diff --git a/src/test/java/spoon/test/GitHubIssue.java b/src/test/java/spoon/test/GitHubIssue.java index f7f8c12c735..0cc3c9e5307 100644 --- a/src/test/java/spoon/test/GitHubIssue.java +++ b/src/test/java/spoon/test/GitHubIssue.java @@ -7,19 +7,19 @@ */ package spoon.test; +import static org.junit.jupiter.api.Assertions.fail; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -import java.util.HashSet; -import java.util.Set; +import java.util.Objects; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.AfterTestExecutionCallback; import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.api.extension.TestExecutionExceptionHandler; -import org.junit.jupiter.api.extension.TestWatcher; -import static org.junit.jupiter.api.Assertions.fail; + @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) @@ -53,32 +53,33 @@ * This is useful to check if each testcase fails as expected. Internal in junit 5 failing testcases simply throw an exception. * Swallowing this exceptions marks the testcase as not failing. */ - static class UnresolvedBugExtension implements TestWatcher, TestExecutionExceptionHandler { - - private Set correctFailingTestCases = new HashSet<>(); - - @Override - public void testSuccessful(ExtensionContext context) { - if (shouldFail(context) && !correctFailingTestCases.contains(context)) { - fail("Method " + context.getTestMethod().get().getName() + " must fail"); - } - } + static class UnresolvedBugExtension implements AfterTestExecutionCallback, TestExecutionExceptionHandler { @Override public void handleTestExecutionException(ExtensionContext context, Throwable throwable) throws Throwable { if (shouldFail(context)) { - correctFailingTestCases.add(context); + context.getStore(ExtensionContext.Namespace.create(GitHubIssue.class)).put("failed", true); return; } // rethrow the exception to fail the test case if it was not expected to fail throw throwable; } + @Override + public void afterTestExecution(ExtensionContext context) throws Exception { + if (shouldFail(context) && !context + .getStore(ExtensionContext.Namespace.create(GitHubIssue.class)) + .getOrDefault("failed", Boolean.class, false)) { + fail("Test " + context.getDisplayName() + " must fail"); + } + } private boolean shouldFail(ExtensionContext context) { return context.getTestMethod() - .map(method -> method.getAnnotation(GitHubIssue.class) != null && !method.getAnnotation(GitHubIssue.class).fixed()) - .orElse(false); + .map(method -> method.getAnnotation(GitHubIssue.class)) + .filter(Objects::nonNull) + .map(v -> !v.fixed()) + .orElse(false); } } } diff --git a/src/test/java/spoon/test/api/Metamodel.java b/src/test/java/spoon/test/api/Metamodel.java index 4faa40650b1..5d88e431cc6 100644 --- a/src/test/java/spoon/test/api/Metamodel.java +++ b/src/test/java/spoon/test/api/Metamodel.java @@ -338,6 +338,7 @@ private static void initTypes(List types) { .field(CtRole.NAME, false, false) .field(CtRole.IS_SHADOW, false, false) .field(CtRole.IS_IMPLICIT, false, false) + .field(CtRole.LABEL, false, false) .field(CtRole.MODIFIER, false, false) .field(CtRole.EMODIFIER, true, true) .field(CtRole.SUPER_TYPE, true, true) @@ -349,6 +350,7 @@ private static void initTypes(List types) { .field(CtRole.INTERFACE, false, false) .field(CtRole.TYPE_PARAMETER, false, false) .field(CtRole.TYPE_MEMBER, false, false) + .field(CtRole.PERMITTED_TYPE, false, false) .field(CtRole.COMMENT, false, false) )); @@ -424,6 +426,7 @@ private static void initTypes(List types) { .field(CtRole.MODIFIER, false, false) .field(CtRole.EMODIFIER, true, true) .field(CtRole.POSITION, false, false) + .field(CtRole.COMPACT_CONSTRUCTOR, false, false) .field(CtRole.ANNOTATION, false, false) .field(CtRole.PARAMETER, false, false) .field(CtRole.THROWN, false, false) @@ -509,6 +512,17 @@ private static void initTypes(List types) { )); + types.add(new Type("CtRecordComponent", spoon.reflect.declaration.CtRecordComponent.class, spoon.support.reflect.declaration.CtRecordComponentImpl.class, fm -> fm + .field(CtRole.NAME, false, false) + .field(CtRole.IS_SHADOW, false, false) + .field(CtRole.IS_IMPLICIT, false, false) + .field(CtRole.POSITION, false, false) + .field(CtRole.ANNOTATION, false, false) + .field(CtRole.TYPE, false, false) + .field(CtRole.COMMENT, false, false) + + )); + types.add(new Type("CtArrayWrite", spoon.reflect.code.CtArrayWrite.class, spoon.support.reflect.code.CtArrayWriteImpl.class, fm -> fm .field(CtRole.IS_IMPLICIT, false, false) .field(CtRole.POSITION, false, false) @@ -671,6 +685,7 @@ private static void initTypes(List types) { .field(CtRole.INTERFACE, false, false) .field(CtRole.TYPE_PARAMETER, false, false) .field(CtRole.TYPE_MEMBER, false, false) + .field(CtRole.PERMITTED_TYPE, false, false) .field(CtRole.COMMENT, false, false) )); @@ -926,6 +941,30 @@ private static void initTypes(List types) { )); + types.add(new Type("CtRecord", spoon.reflect.declaration.CtRecord.class, spoon.support.reflect.declaration.CtRecordImpl.class, fm -> fm + .field(CtRole.NAME, false, false) + .field(CtRole.IS_SHADOW, false, false) + .field(CtRole.IS_IMPLICIT, false, false) + .field(CtRole.LABEL, true, true) + .field(CtRole.MODIFIER, false, false) + .field(CtRole.EMODIFIER, true, true) + .field(CtRole.SUPER_TYPE, true, true) + .field(CtRole.NESTED_TYPE, true, false) + .field(CtRole.CONSTRUCTOR, true, false) + .field(CtRole.METHOD, true, false) + .field(CtRole.ANNONYMOUS_EXECUTABLE, true, false) + .field(CtRole.FIELD, true, false) + .field(CtRole.POSITION, false, false) + .field(CtRole.PERMITTED_TYPE, true, true) + .field(CtRole.ANNOTATION, false, false) + .field(CtRole.INTERFACE, false, false) + .field(CtRole.TYPE_MEMBER, false, false) + .field(CtRole.TYPE_PARAMETER, false, false) + .field(CtRole.RECORD_COMPONENT, false, false) + .field(CtRole.COMMENT, false, false) + + )); + types.add(new Type("CtModule", spoon.reflect.declaration.CtModule.class, spoon.support.reflect.declaration.CtModuleImpl.class, fm -> fm .field(CtRole.NAME, false, false) .field(CtRole.IS_IMPLICIT, false, false) @@ -1287,6 +1326,7 @@ private static void initTypes(List types) { .field(CtRole.FIELD, true, false) .field(CtRole.TYPE_PARAMETER, true, true) .field(CtRole.POSITION, false, false) + .field(CtRole.PERMITTED_TYPE, true, true) .field(CtRole.ANNOTATION, false, false) .field(CtRole.INTERFACE, false, false) .field(CtRole.TYPE_MEMBER, false, false) @@ -1365,36 +1405,5 @@ private static void initTypes(List types) { .field(CtRole.TARGET, false, false) )); - - types.add(new Type("CtRecord", spoon.reflect.declaration.CtRecord.class, spoon.support.reflect.declaration.CtRecordImpl.class, fm -> fm - .field(CtRole.NAME, false, false) - .field(CtRole.IS_SHADOW, false, false) - .field(CtRole.IS_IMPLICIT, false, false) - .field(CtRole.LABEL, true, true) - .field(CtRole.MODIFIER, false, false) - .field(CtRole.EMODIFIER, true, true) - .field(CtRole.SUPER_TYPE, true, true) - .field(CtRole.NESTED_TYPE, true, false) - .field(CtRole.CONSTRUCTOR, true, false) - .field(CtRole.METHOD, true, false) - .field(CtRole.ANNONYMOUS_EXECUTABLE, true, false) - .field(CtRole.FIELD, true, false) - .field(CtRole.POSITION, false, false) - .field(CtRole.ANNOTATION, false, false) - .field(CtRole.INTERFACE, false, false) - .field(CtRole.TYPE_MEMBER, false, false) - .field(CtRole.TYPE_PARAMETER, false, false) - .field(CtRole.RECORD_COMPONENT, false, false) - .field(CtRole.COMMENT, false, false) - )); - - types.add(new Type("CtRecordComponent", spoon.reflect.declaration.CtRecordComponent.class, spoon.support.reflect.declaration.CtRecordComponentImpl.class, fm -> fm - .field(CtRole.NAME, false, false) - .field(CtRole.IS_IMPLICIT, false, false) - .field(CtRole.POSITION, false, false) - .field(CtRole.ANNOTATION, false, false) - .field(CtRole.TYPE, false, false) - .field(CtRole.COMMENT, false, false) - )); } } diff --git a/src/test/java/spoon/test/enums/EnumsTest.java b/src/test/java/spoon/test/enums/EnumsTest.java index 7658e2d006d..98e59b209f6 100644 --- a/src/test/java/spoon/test/enums/EnumsTest.java +++ b/src/test/java/spoon/test/enums/EnumsTest.java @@ -18,6 +18,7 @@ import org.apache.commons.lang3.StringUtils; import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.params.ParameterizedTest; @@ -28,8 +29,10 @@ import spoon.reflect.CtModel; import spoon.reflect.code.CtBlock; import spoon.reflect.code.CtExpression; +import spoon.reflect.code.CtInvocation; import spoon.reflect.code.CtNewClass; import spoon.reflect.code.CtStatement; +import spoon.reflect.declaration.CtConstructor; import spoon.reflect.declaration.CtEnum; import spoon.reflect.declaration.CtEnumValue; import spoon.reflect.declaration.CtField; @@ -37,8 +40,10 @@ import spoon.reflect.declaration.CtType; import spoon.reflect.declaration.ModifierKind; import spoon.reflect.factory.Factory; +import spoon.reflect.reference.CtExecutableReference; import spoon.reflect.visitor.filter.TypeFilter; import spoon.support.reflect.CtExtendedModifier; +import spoon.test.GitHubIssue; import spoon.test.SpoonTestHelpers; import spoon.test.annotation.AnnotationTest; import spoon.test.enums.testclasses.Burritos; @@ -58,6 +63,7 @@ import static org.hamcrest.CoreMatchers.hasItem; import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.not; import static org.hamcrest.MatcherAssert.assertThat; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertSame; @@ -222,6 +228,22 @@ void testEnumClassModifiersPublicEnum() throws Exception { )); } + @Test + void testEnumClassModifiersPublicEnumJava17() throws Exception { + // contract: enum modifiers are applied correctly (JLS 8.9) + // in Java 17, enums are implicitly sealed if an enum value declares an anonymous type + // and the permitted types are the anonymous types declared by the enum values + Launcher launcher = new Launcher(); + launcher.addInputResource("./src/test/java/spoon/test/enums/testclasses/AnonEnum.java"); + launcher.getEnvironment().setComplianceLevel(17); + launcher.buildModel(); + CtType publicEnum = launcher.getFactory().Type().get("spoon.test.enums.testclasses.AnonEnum"); + assertThat(publicEnum.getExtendedModifiers(), contentEquals( + new CtExtendedModifier(ModifierKind.PUBLIC, false), + new CtExtendedModifier(ModifierKind.SEALED, true) + )); + } + @Test void testEnumValueModifiers() throws Exception { // contract: anonymous enum classes are always final @@ -264,6 +286,25 @@ void testLocalEnumExists() { )); } + @GitHubIssue(issueNumber = 4758, fixed = true) + @DisplayName("Implicit enum constructors do not contain a super call") + void testImplicitEnumConstructorSuperCall() { + CtEnum myEnum = (CtEnum) Launcher.parseClass("enum Foo { CONSTANT; }"); + CtConstructor constructor = myEnum.getConstructors().iterator().next(); + + assertThat(constructor.isImplicit(), is(true)); + + for (CtStatement statement : constructor.getBody().getStatements()) { + if (!(statement instanceof CtInvocation)) { + continue; + } + CtExecutableReference executable = ((CtInvocation) statement).getExecutable(); + if (!executable.getDeclaringType().getQualifiedName().equals("java.lang.Enum")) { + continue; + } + assertThat(executable.getSimpleName(), not(is(""))); + } + } static class NestedEnumTypeProvider implements ArgumentsProvider { private final CtType ctClass; diff --git a/src/test/java/spoon/test/field/FieldTest.java b/src/test/java/spoon/test/field/FieldTest.java index 81c9287f76b..935c6d13810 100644 --- a/src/test/java/spoon/test/field/FieldTest.java +++ b/src/test/java/spoon/test/field/FieldTest.java @@ -45,6 +45,7 @@ import spoon.test.field.testclasses.AddFieldAtTop; import spoon.test.field.testclasses.BaseClass; import spoon.testing.utils.LineSeperatorExtension; +import spoon.testing.utils.ModelTest; public class FieldTest { @@ -127,14 +128,12 @@ private CtField createField(Factory factory, HashSet modi return first; } - @Test - public void testGetDefaultExpression() { - Launcher spoon = new Launcher(); - spoon.addInputResource("./src/test/java/spoon/test/field/testclasses/A.java"); - spoon.addInputResource("./src/test/java/spoon/test/field/testclasses/BaseClass.java"); - spoon.buildModel(); - - final CtClass aClass = spoon.getFactory().Class().get(A.class); + @ModelTest({ + "./src/test/java/spoon/test/field/testclasses/A.java", + "./src/test/java/spoon/test/field/testclasses/BaseClass.java", + }) + public void testGetDefaultExpression(Factory factory) { + final CtClass aClass = factory.Class().get(A.class); // contract: isPartOfJointDeclaration works per the specification in the javadoc assertEquals(false,aClass.getField("alone1").isPartOfJointDeclaration()); @@ -175,14 +174,10 @@ public void testGetDefaultExpression() { assertNotNull(retour); } - @Test - public void getFQNofFieldReference() { + @ModelTest("./src/test/resources/spoon/test/noclasspath/fields/Toto.java") + public void getFQNofFieldReference(CtModel model) { // contract: when a reference field origin cannot be determined a call to its qualified name returns an explicit value - Launcher launcher = new Launcher(); - launcher.addInputResource("./src/test/resources/spoon/test/noclasspath/fields/Toto.java"); - launcher.getEnvironment().setNoClasspath(true); - CtModel ctModel = launcher.buildModel(); - List elements = ctModel.getElements(new TypeFilter<>(CtFieldReference.class)); + List elements = model.getElements(new TypeFilter<>(CtFieldReference.class)); assertEquals(1, elements.size()); CtFieldReference fieldReference = elements.get(0); diff --git a/src/test/java/spoon/test/filters/CUFilterTest.java b/src/test/java/spoon/test/filters/CUFilterTest.java index a277a8ddc42..103394825f1 100644 --- a/src/test/java/spoon/test/filters/CUFilterTest.java +++ b/src/test/java/spoon/test/filters/CUFilterTest.java @@ -22,21 +22,18 @@ import spoon.reflect.code.CtConstructorCall; import spoon.reflect.code.CtReturn; import spoon.support.compiler.jdt.CompilationUnitFilter; +import spoon.testing.utils.ModelTest; import static org.junit.jupiter.api.Assertions.assertEquals; public class CUFilterTest { - @Test - public void testWithoutFilters() { - final Launcher launcher = new Launcher(); - launcher.addInputResource("./src/test/resources/noclasspath/same-package"); - launcher.buildModel(); - final CtModel model = launcher.getModel(); - assertEquals(2, model.getAllTypes().size()); - assertEquals("spoon.test.same.B", model.getAllTypes().iterator().next() - .getMethod("createB").getType().getQualifiedName()); - } + @ModelTest("./src/test/resources/noclasspath/same-package") + public void testWithoutFilters(CtModel model) { + assertEquals(2, model.getAllTypes().size()); + assertEquals("spoon.test.same.B", model.getAllTypes().iterator().next() + .getMethod("createB").getType().getQualifiedName()); + } @Test public void testSingleExcludeWithFilter() { diff --git a/src/test/java/spoon/test/filters/FilterTest.java b/src/test/java/spoon/test/filters/FilterTest.java index 8a2af8bceb4..7a76af96ea9 100644 --- a/src/test/java/spoon/test/filters/FilterTest.java +++ b/src/test/java/spoon/test/filters/FilterTest.java @@ -20,6 +20,7 @@ import org.junit.jupiter.api.Test; import spoon.Launcher; import spoon.legacy.NameFilter; +import spoon.reflect.CtModel; import spoon.reflect.code.CtCFlowBreak; import spoon.reflect.code.CtExpression; import spoon.reflect.code.CtFieldAccess; @@ -87,6 +88,7 @@ import spoon.test.filters.testclasses.Tacos; import spoon.test.filters.testclasses.Tostada; import spoon.test.imports.testclasses.internal4.Constants; +import spoon.testing.utils.ModelTest; import spoon.testing.utils.ModelUtils; import java.util.ArrayList; @@ -1379,15 +1381,10 @@ public void accept(CtType ctType) { assertEquals(1, c2.counter); } - @Test - public void testNameFilterWithGenericType() { + @ModelTest("./src/test/java/spoon/test/imports/testclasses/internal4/Constants.java") + public void testNameFilterWithGenericType(Factory factory) { // contract: NamedElementFilter of T should only return T elements - - Launcher spoon = new Launcher(); - spoon.addInputResource("./src/test/java/spoon/test/imports/testclasses/internal4/Constants.java"); - spoon.buildModel(); - - CtType type = spoon.getFactory().Type().get(Constants.class); + CtType type = factory.Type().get(Constants.class); List ctMethods = type.getElements(new NamedElementFilter<>(CtMethod.class, "CONSTANT")); assertTrue(ctMethods.isEmpty()); @@ -1396,31 +1393,21 @@ public void testNameFilterWithGenericType() { assertTrue(ctFields.get(0) instanceof CtField); } - @Test - public void testSubTypeFilter() { + @ModelTest("./src/test/java/spoon/test/filters/testclasses") + public void testSubTypeFilter(Factory factory, CtModel model) { // contract: SubtypeFilter correctly filters subtypes - - Launcher spoon = new Launcher(); - spoon.addInputResource("./src/test/java/spoon/test/filters/testclasses"); - spoon.buildModel(); - - CtType type = spoon.getFactory().Type().get(AbstractTostada.class); - List> types = spoon.getModel().getRootPackage().getElements(new SubtypeFilter(type.getReference())); + CtType type = factory.Type().get(AbstractTostada.class); + List> types = model.getRootPackage().getElements(new SubtypeFilter(type.getReference())); assertEquals(6, types.size()); - List> types2 = spoon.getModel().getRootPackage().getElements(new SubtypeFilter(type.getReference()).includingSelf(false)); + List> types2 = model.getRootPackage().getElements(new SubtypeFilter(type.getReference()).includingSelf(false)); assertEquals(5, types2.size()); } - @Test - public void testFilterContains() { + @ModelTest("./src/test/java/spoon/test/filters/testclasses") + public void testFilterContains(Factory factory) { // ref: https://github.com/INRIA/spoon/issues/3058 - - Launcher spoon = new Launcher(); - spoon.addInputResource("./src/test/java/spoon/test/filters/testclasses"); - spoon.buildModel(); - - CtType type = spoon.getFactory().Type().get(Foo.class); + CtType type = factory.Type().get(Foo.class); CtStatement s = type.getMethodsByName("foo").get(0).getBody().getStatement(0); assertEquals("int x = 3", s.toString()); @@ -1434,7 +1421,7 @@ public boolean matches(CtElement element) { } // we look for this AST in the new class - List l = spoon.getFactory().Type().get(FooLine.class).filterChildren(new ContainFilter(s)).list(); + List l = factory.Type().get(FooLine.class).filterChildren(new ContainFilter(s)).list(); assertEquals(1, l.size()); diff --git a/src/test/java/spoon/test/imports/ImportScannerTest.java b/src/test/java/spoon/test/imports/ImportScannerTest.java index 0bebb4d342b..1a9525fa5ed 100644 --- a/src/test/java/spoon/test/imports/ImportScannerTest.java +++ b/src/test/java/spoon/test/imports/ImportScannerTest.java @@ -20,6 +20,7 @@ import spoon.Launcher; import spoon.SpoonModelBuilder; import spoon.compiler.SpoonResourceHelper; +import spoon.reflect.CtModel; import spoon.reflect.declaration.CtClass; import spoon.reflect.declaration.CtImport; import spoon.reflect.declaration.CtType; @@ -33,6 +34,7 @@ import spoon.reflect.visitor.filter.NamedElementFilter; import spoon.support.JavaOutputProcessor; import spoon.test.imports.testclasses.ToBeModified; +import spoon.testing.utils.ModelTest; import spoon.testing.utils.ModelUtils; import java.io.File; @@ -54,36 +56,30 @@ public class ImportScannerTest { - @Test - public void testImportOnSpoon() throws IOException { + @ModelTest("./src/main/java/spoon/") + public void testImportOnSpoon(Launcher launcher, Factory factory, CtModel model) throws IOException { File targetDir = new File("./target/import-test"); - Launcher spoon = new Launcher(); - spoon.addInputResource("./src/main/java/spoon/"); - spoon.getEnvironment().setAutoImports(true); - spoon.getEnvironment().setCommentEnabled(true); - spoon.getEnvironment().setSourceOutputDirectory(targetDir); - spoon.getEnvironment().setLevel("warn"); - spoon.buildModel(); + launcher.getEnvironment().setSourceOutputDirectory(targetDir); // contract: ImportScannerImpl does not throw an exception on a large and complex model (OK, it's a smoke test) // Update: I found a nasty NPE bug :-), smoke tests can be useful - new ImportScannerImpl().scan(spoon.getFactory().Package().getRootPackage()); + new ImportScannerImpl().scan(factory.Package().getRootPackage()); - PrettyPrinter prettyPrinter = new DefaultJavaPrettyPrinter(spoon.getEnvironment()); + PrettyPrinter prettyPrinter = new DefaultJavaPrettyPrinter(launcher.getEnvironment()); Map> missingImports = new HashMap<>(); Map> unusedImports = new HashMap<>(); JavaOutputProcessor outputProcessor; - for (CtType ctType : spoon.getModel().getAllTypes()) { + for (CtType ctType : model.getAllTypes()) { if (!ctType.isTopLevel()) { continue; } outputProcessor = new JavaOutputProcessor(prettyPrinter); - outputProcessor.setFactory(spoon.getFactory()); + outputProcessor.setFactory(factory); outputProcessor.init(); Set computedTypeImports = new HashSet<>(); @@ -246,17 +242,12 @@ public void testComputeImportsInClass() throws Exception { assertEquals(2, imports.size()); } - @Test - public void testComputeImportsInClassWithSameName() { + @ModelTest("src/test/resources/spoon/test/imports/testclasses2/") + public void testComputeImportsInClassWithSameName(Factory factory) { String packageName = "spoon.test.imports.testclasses2"; String className = "ImportSameName"; String qualifiedName = packageName + "." + className; - - Launcher spoon = new Launcher(); - spoon.addInputResource("src/test/resources/spoon/test/imports/testclasses2/"); - spoon.buildModel(); - Factory aFactory = spoon.getFactory(); - CtType theClass = aFactory.Type().get(qualifiedName); + CtType theClass = factory.Type().get(qualifiedName); ImportScanner importContext = new ImportScannerImpl(); importContext.computeImports(theClass); diff --git a/src/test/java/spoon/test/imports/ImportTest.java b/src/test/java/spoon/test/imports/ImportTest.java index e0cd637d92c..e5029155c93 100644 --- a/src/test/java/spoon/test/imports/ImportTest.java +++ b/src/test/java/spoon/test/imports/ImportTest.java @@ -71,6 +71,7 @@ import spoon.test.imports.testclasses.Tacos; import spoon.test.imports.testclasses.ToBeModified; import spoon.test.imports.testclasses.badimportissue3320.source.TestSource; +import spoon.testing.utils.ModelTest; import spoon.testing.utils.ModelUtils; import java.io.BufferedReader; @@ -1289,17 +1290,9 @@ public void testImportStarredPackageWithNonVisibleClass() throws IOException { assertEquals(CtImportKind.ALL_TYPES, cu.getImports().iterator().next().getImportKind()); } - @Test - public void testImportWithGenerics() { + @ModelTest(value = "./src/test/resources/import-with-generics/TestWithGenerics.java", autoImport = true) + public void testImportWithGenerics(Launcher launcher) { // contract: in noclasspath autoimport, we should be able to use generic type - final Launcher launcher = new Launcher(); - // this class is not compilable 'spoon.test.imports.testclasses.withgenerics.Target' does not exist - launcher.addInputResource("./src/test/resources/import-with-generics/TestWithGenerics.java"); - launcher.getEnvironment().setAutoImports(true); - launcher.getEnvironment().setNoClasspath(true); - launcher.setSourceOutputDirectory("./target/import-with-generics"); - launcher.run(); - PrettyPrinter prettyPrinter = launcher.createPrettyPrinter(); CtType element = launcher.getFactory().Class().get("spoon.test.imports.testclasses.TestWithGenerics"); List> toPrint = new ArrayList<>(); @@ -1441,12 +1434,9 @@ public void testNullable() throws Exception { assertNotNull(launcher.getFactory().Type().get("TestNullable")); } - @Test - public void testBug2369_fqn() { + @ModelTest("./src/test/java/spoon/test/imports/testclasses/JavaLongUse.java") + public void testBug2369_fqn(Factory factory) { // see https://github.com/INRIA/spoon/issues/2369 - final Launcher launcher = new Launcher(); -launcher.addInputResource("./src/test/java/spoon/test/imports/testclasses/JavaLongUse.java"); - launcher.buildModel(); final String nl = System.lineSeparator(); assertEquals("public class JavaLongUse {" + nl + " public class Long {}" + nl + @@ -1458,16 +1448,12 @@ public void testBug2369_fqn() { " public static void main(java.lang.String[] args) {" + nl + " java.lang.System.out.println(spoon.test.imports.testclasses.JavaLongUse.method());" + nl + " }" + nl + - "}", launcher.getFactory().Type().get("spoon.test.imports.testclasses.JavaLongUse").toString()); + "}", factory.Type().get("spoon.test.imports.testclasses.JavaLongUse").toString()); } - @Test - public void testBug2369_autoimports() { + @ModelTest(value = "./src/test/java/spoon/test/imports/testclasses/JavaLongUse.java", autoImport = true) + public void testBug2369_autoimports(Launcher launcher) { // https://github.com/INRIA/spoon/issues/2369 - final Launcher launcher = new Launcher(); - launcher.addInputResource("./src/test/java/spoon/test/imports/testclasses/JavaLongUse.java"); - launcher.getEnvironment().setAutoImports(true); - launcher.buildModel(); final String nl = System.lineSeparator(); assertEquals("public class JavaLongUse {" + nl + " public class Long {}" + nl + @@ -1581,31 +1567,23 @@ public void testFQNJavadoc() { assertThat(output, containsString("import spoon.SpoonException;")); } } - @Test - public void testImportOnSpoon() throws IOException { - File targetDir = new File("./target/import-test"); - Launcher spoon = new Launcher(); - spoon.addInputResource("./src/main/java/spoon/"); - spoon.getEnvironment().setAutoImports(true); - spoon.getEnvironment().setCommentEnabled(true); - spoon.getEnvironment().setSourceOutputDirectory(targetDir); - spoon.getEnvironment().setLevel("warn"); - spoon.buildModel(); - PrettyPrinter prettyPrinter = new DefaultJavaPrettyPrinter(spoon.getEnvironment()); + @ModelTest(value = "./src/main/java/spoon/", autoImport = true) + public void testImportOnSpoon(Launcher launcher, CtModel model, Factory factory) throws IOException { + PrettyPrinter prettyPrinter = new DefaultJavaPrettyPrinter(launcher.getEnvironment()); Map> missingImports = new HashMap<>(); Map> unusedImports = new HashMap<>(); JavaOutputProcessor outputProcessor; - for (CtType ctType : spoon.getModel().getAllTypes()) { + for (CtType ctType : model.getAllTypes()) { if (!ctType.isTopLevel()) { continue; } outputProcessor = new JavaOutputProcessor(prettyPrinter); - outputProcessor.setFactory(spoon.getFactory()); + outputProcessor.setFactory(factory); outputProcessor.init(); Set computedTypeImports = new HashSet<>(); diff --git a/src/test/java/spoon/test/initializers/InitializerTest.java b/src/test/java/spoon/test/initializers/InitializerTest.java index ec927840bb1..e55be37befc 100644 --- a/src/test/java/spoon/test/initializers/InitializerTest.java +++ b/src/test/java/spoon/test/initializers/InitializerTest.java @@ -17,7 +17,6 @@ package spoon.test.initializers; import org.junit.jupiter.api.Test; -import spoon.Launcher; import spoon.reflect.CtModel; import spoon.reflect.code.CtConstructorCall; import spoon.reflect.code.CtLiteral; @@ -28,6 +27,7 @@ import spoon.reflect.visitor.filter.NamedElementFilter; import spoon.reflect.visitor.filter.TypeFilter; import spoon.test.imports.ImportTest; +import spoon.testing.utils.ModelTest; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; @@ -74,13 +74,8 @@ public void testModelBuildingInitializer() throws Exception { assertEquals("x = 3", ex.getBody().getStatements().get(0).toString()); } - @Test - public void testModelBuildingInitializerNoclasspath() { - Launcher launcher = new Launcher(); - launcher.addInputResource("./src/test/resources/noclasspath/initializer/Utf8HttpResponse.java"); - launcher.getEnvironment().setNoClasspath(true); - launcher.getEnvironment().setAutoImports(true); - CtModel model = launcher.buildModel(); + @ModelTest(value = "./src/test/resources/noclasspath/initializer/Utf8HttpResponse.java", autoImport = true) + public void testModelBuildingInitializerNoclasspath(CtModel model) { CtClass ctClass = model.getElements(new NamedElementFilter<>(CtClass.class, "Utf8HttpResponse")).get(0); CtAnonymousExecutable ex = ctClass.getElements(new TypeFilter<>(CtAnonymousExecutable.class)).get(0); diff --git a/src/test/java/spoon/test/interfaces/InterfaceTest.java b/src/test/java/spoon/test/interfaces/InterfaceTest.java index b30cc2047af..1b4bdcc24a3 100644 --- a/src/test/java/spoon/test/interfaces/InterfaceTest.java +++ b/src/test/java/spoon/test/interfaces/InterfaceTest.java @@ -38,6 +38,7 @@ import spoon.reflect.declaration.CtMethod; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import spoon.testing.utils.ModelTest; import java.util.function.Consumer; import java.util.stream.Collectors; @@ -183,15 +184,10 @@ void testPackageLevelInterfaceModifiers() throws Exception { )); } - @Test - public void testNestedTypesInInterfaceArePublic() { + @ModelTest("src/test/resources/nestedInInterface") + public void testNestedTypesInInterfaceArePublic(CtModel model) { // contract: nested types in interfaces are implicitly public // (https://docs.oracle.com/javase/specs/jls/se16/html/jls-9.html#jls-9.5) - - Launcher launcher = new Launcher(); - launcher.addInputResource("src/test/resources/nestedInInterface"); - CtModel model = launcher.buildModel(); - Collection> types = model.getAllTypes() .stream() .flatMap(it -> it.getNestedTypes().stream()) @@ -211,15 +207,10 @@ public void testNestedTypesInInterfaceArePublic() { } } - @Test - public void testNestedTypesInInterfaceAreStatic() { + @ModelTest("src/test/resources/nestedInInterface") + public void testNestedTypesInInterfaceAreStatic(CtModel model) { // contract: nested types in interfaces are implicitly static // (https://docs.oracle.com/javase/specs/jls/se16/html/jls-9.html#jls-9.5) - - Launcher launcher = new Launcher(); - launcher.addInputResource("src/test/resources/nestedInInterface"); - CtModel model = launcher.buildModel(); - Collection> types = model.getAllTypes() .stream() .flatMap(it -> it.getNestedTypes().stream()) diff --git a/src/test/java/spoon/test/interfaces/TestInterfaceWithoutSetup.java b/src/test/java/spoon/test/interfaces/TestInterfaceWithoutSetup.java index 5f2713ef3a6..ff0b1e556bd 100644 --- a/src/test/java/spoon/test/interfaces/TestInterfaceWithoutSetup.java +++ b/src/test/java/spoon/test/interfaces/TestInterfaceWithoutSetup.java @@ -16,6 +16,7 @@ */ package spoon.test.interfaces; +import spoon.reflect.factory.Factory; import spoon.support.reflect.CtExtendedModifier; import spoon.reflect.declaration.ModifierKind; import spoon.reflect.declaration.CtType; @@ -24,6 +25,7 @@ import spoon.Launcher; import spoon.reflect.declaration.CtMethod; import org.junit.jupiter.api.Test; +import spoon.testing.utils.ModelTest; import java.util.Set; import java.io.IOException; @@ -36,15 +38,10 @@ public class TestInterfaceWithoutSetup { - @Test - public void testModifierFromInterfaceFieldAndMethod() { + @ModelTest(value = "./src/test/resources/spoon/test/itf/DumbItf.java", noClasspath = false) + public void testModifierFromInterfaceFieldAndMethod(Factory factory) { // contract: methods defined in interface are all public and fields are all public and static - Launcher spoon = new Launcher(); - spoon.addInputResource("./src/test/resources/spoon/test/itf/DumbItf.java"); - spoon.getEnvironment().setNoClasspath(false); - spoon.buildModel(); - - CtType dumbType = spoon.getFactory().Type().get("toto.DumbItf"); + CtType dumbType = factory.Type().get("toto.DumbItf"); assertEquals(2, dumbType.getFields().size()); diff --git a/src/test/java/spoon/test/javadoc/JavaDocTest.java b/src/test/java/spoon/test/javadoc/JavaDocTest.java index 31e58150f1d..25e32b6f091 100644 --- a/src/test/java/spoon/test/javadoc/JavaDocTest.java +++ b/src/test/java/spoon/test/javadoc/JavaDocTest.java @@ -38,6 +38,7 @@ import spoon.reflect.visitor.filter.TypeFilter; import spoon.test.imports.ImportTest; import spoon.test.javadoc.testclasses.Bar; +import spoon.testing.utils.ModelTest; import java.util.List; @@ -141,15 +142,9 @@ public void testBugSetContent() { assertEquals("foo", j.getTags().get(0).getContent()); } - @Test - public void testTagsParameters() { + @ModelTest("./src/test/java/spoon/test/javadoc/testclasses/A.java") + public void testTagsParameters(CtModel model) { // contract: @throws and @exception should have proper params - Launcher launcher = new Launcher(); - launcher.addInputResource("./src/test/java/spoon/test/javadoc/testclasses/A.java"); - launcher.getEnvironment().setCommentEnabled(true); - launcher.getEnvironment().setNoClasspath(true); - CtModel model = launcher.buildModel(); - List javadocs = model.getElements(new TypeFilter<>(CtJavaDoc.class)); CtJavaDocTag throwsTag = javadocs.get(0).getTags().get(0); @@ -159,15 +154,9 @@ public void testTagsParameters() { assertEquals("FileNotFoundException", exceptionTag.getParam()); } - @Test - public void testJavadocTagNames() { + @ModelTest("./src/test/java/spoon/test/javadoc/testclasses/B.java") + public void testJavadocTagNames(CtModel model) { //contract: we should handle all possible javadoc tags properly - Launcher launcher = new Launcher(); - launcher.addInputResource("./src/test/java/spoon/test/javadoc/testclasses/B.java"); - launcher.getEnvironment().setCommentEnabled(true); - launcher.getEnvironment().setNoClasspath(true); - CtModel model = launcher.buildModel(); - CtType type = model.getAllTypes().stream().findFirst().get(); assertEquals(TagType.VERSION, type.getElements(new TypeFilter<>(CtEnum.class)).get(0).getElements(new TypeFilter<>(CtJavaDoc.class)).get(0).getTags().get(0).getType()); assertEquals(TagType.AUTHOR, type.getMethodsByName("m1").get(0).getElements(new TypeFilter<>(CtJavaDoc.class)).get(0).getTags().get(0).getType()); diff --git a/src/test/java/spoon/test/jdtimportbuilder/ImportBuilderTest.java b/src/test/java/spoon/test/jdtimportbuilder/ImportBuilderTest.java index ee2895eaa0c..71863b07ec3 100644 --- a/src/test/java/spoon/test/jdtimportbuilder/ImportBuilderTest.java +++ b/src/test/java/spoon/test/jdtimportbuilder/ImportBuilderTest.java @@ -16,6 +16,7 @@ */ package spoon.test.jdtimportbuilder; +import spoon.reflect.factory.Factory; import spoon.test.imports.testclasses.A; import spoon.reflect.reference.CtFieldReference; import spoon.test.jdtimportbuilder.testclasses.StaticImport; @@ -31,6 +32,7 @@ import spoon.test.jdtimportbuilder.testclasses.StarredImport; import spoon.test.imports.testclasses.ClassWithInvocation; import org.junit.jupiter.api.Test; +import spoon.testing.utils.ModelTest; import java.util.stream.Collectors; import java.util.Set; @@ -45,29 +47,19 @@ */ public class ImportBuilderTest { - @Test - public void testWithNoImport() { + @ModelTest(value = "./src/test/java/spoon/test/imports/testclasses/A.java", autoImport = true) + public void testWithNoImport(Factory factory) { // contract: when the source code has no import, none is created when building model - Launcher spoon = new Launcher(); - spoon.addInputResource("./src/test/java/spoon/test/imports/testclasses/A.java"); - spoon.getEnvironment().setAutoImports(true); - spoon.buildModel(); - - CtClass classA = spoon.getFactory().Class().get(A.class); - CompilationUnit unitA = spoon.getFactory().CompilationUnit().getMap().get(classA.getPosition().getFile().getPath()); + CtClass classA = factory.Class().get(A.class); + CompilationUnit unitA = factory.CompilationUnit().getMap().get(classA.getPosition().getFile().getPath()); assertTrue(unitA.getImports().isEmpty()); } - @Test - public void testWithSimpleImport() { + @ModelTest(value = "./src/test/java/spoon/test/imports/testclasses/ClassWithInvocation.java", autoImport = true) + public void testWithSimpleImport(Factory factory) { // contract: when the source has one import, the same import is created as a reference in auto-import mode - Launcher spoon = new Launcher(); - spoon.addInputResource("./src/test/java/spoon/test/imports/testclasses/ClassWithInvocation.java"); - spoon.getEnvironment().setAutoImports(true); - spoon.buildModel(); - - CtClass classA = spoon.getFactory().Class().get(ClassWithInvocation.class); - CompilationUnit unitA = spoon.getFactory().CompilationUnit().getMap().get(classA.getPosition().getFile().getPath()); + CtClass classA = factory.Class().get(ClassWithInvocation.class); + CompilationUnit unitA = factory.CompilationUnit().getMap().get(classA.getPosition().getFile().getPath()); Collection imports = unitA.getImports(); assertEquals(1, imports.size()); @@ -94,17 +86,11 @@ public void testWithSimpleImportNoAutoimport() { assertTrue(unitA.getImports().isEmpty()); } - @Test - public void testInternalImportWhenNoClasspath() { + @ModelTest(value = "./src/test/resources/noclasspath/Attachment.java", autoImport = true) + public void testInternalImportWhenNoClasspath(Factory factory) { // contract: in no-classpath anything which is not loaded becomes CtUnresolvedImport, even if original source code has imports - Launcher spoon = new Launcher(); - spoon.addInputResource("./src/test/resources/noclasspath/Attachment.java"); - spoon.getEnvironment().setAutoImports(true); - spoon.getEnvironment().setNoClasspath(true); - spoon.buildModel(); - - CtClass classA = spoon.getFactory().Class().get("it.feio.android.omninotes.models.Attachment"); - CompilationUnit unitA = spoon.getFactory().CompilationUnit().getMap().get(classA.getPosition().getFile().getPath()); + CtClass classA = factory.Class().get("it.feio.android.omninotes.models.Attachment"); + CompilationUnit unitA = factory.CompilationUnit().getMap().get(classA.getPosition().getFile().getPath()); assertTrue(unitA.getImports().stream().filter(i -> !(i instanceof CtUnresolvedImport)).collect(Collectors.toList()).isEmpty()); assertEquals(3, unitA.getImports().size()); @@ -115,16 +101,11 @@ public void testInternalImportWhenNoClasspath() { assertTrue(importRefs.contains("android.os.Parcelable")); } - @Test - public void testSimpleStaticImport() { + @ModelTest(value = "./src/test/java/spoon/test/jdtimportbuilder/testclasses/StaticImport.java", autoImport = true) + public void testSimpleStaticImport(Factory factory) { // contract: simple static import are imported correctly - Launcher spoon = new Launcher(); - spoon.addInputResource("./src/test/java/spoon/test/jdtimportbuilder/testclasses/StaticImport.java"); - spoon.getEnvironment().setAutoImports(true); - spoon.buildModel(); - - CtClass classA = spoon.getFactory().Class().get(StaticImport.class); - CompilationUnit unitA = spoon.getFactory().CompilationUnit().getMap().get(classA.getPosition().getFile().getPath()); + CtClass classA = factory.Class().get(StaticImport.class); + CompilationUnit unitA = factory.CompilationUnit().getMap().get(classA.getPosition().getFile().getPath()); Collection imports = unitA.getImports(); assertEquals(1, imports.size()); @@ -135,17 +116,17 @@ public void testSimpleStaticImport() { assertEquals("spoon.test.jdtimportbuilder.testclasses.staticimport.Dependency#ANY", ((CtFieldReference) ref.getReference()).getQualifiedName()); } - @Test - public void testWithStaticStarredImportFromInterface() { + @ModelTest( + value = { + "./src/test/java/spoon/test/jdtimportbuilder/testclasses/StarredImport.java", + "./src/test/java/spoon/test/jdtimportbuilder/testclasses/fullpack/", + }, + autoImport = true + ) + public void testWithStaticStarredImportFromInterface(Factory factory) { // contract: when a starred import is used with a target package, all classes of the package should be imported - Launcher spoon = new Launcher(); - spoon.addInputResource("./src/test/java/spoon/test/jdtimportbuilder/testclasses/StarredImport.java"); - spoon.addInputResource("./src/test/java/spoon/test/jdtimportbuilder/testclasses/fullpack/"); - spoon.getEnvironment().setAutoImports(true); - spoon.buildModel(); - - CtClass classA = spoon.getFactory().Class().get(StarredImport.class); - CompilationUnit unitA = spoon.getFactory().CompilationUnit().getMap().get(classA.getPosition().getFile().getPath()); + CtClass classA = factory.Class().get(StarredImport.class); + CompilationUnit unitA = factory.CompilationUnit().getMap().get(classA.getPosition().getFile().getPath()); Collection imports = unitA.getImports(); assertEquals(1, imports.size()); @@ -161,19 +142,17 @@ public void testWithStaticStarredImportFromInterface() { assertEquals("spoon.test.jdtimportbuilder.testclasses.fullpack", ref.getQualifiedName()); } - @Test - public void testWithStaticInheritedImport() { + @ModelTest( + value = { + "./src/test/java/spoon/test/jdtimportbuilder/testclasses/StaticImportWithInheritance.java", + "./src/test/java/spoon/test/jdtimportbuilder/testclasses/staticimport", + }, + autoImport = true + ) + public void testWithStaticInheritedImport(Factory factory) { // contract: When using starred static import of a type, it imports a starred type - Launcher spoon = new Launcher(); - spoon.addInputResource("./src/test/java/spoon/test/jdtimportbuilder/testclasses/StaticImportWithInheritance.java"); - spoon.addInputResource("./src/test/java/spoon/test/jdtimportbuilder/testclasses/staticimport"); - spoon.getEnvironment().setAutoImports(true); - spoon.getEnvironment().setShouldCompile(true); - spoon.setSourceOutputDirectory("./target/spoon-jdtimport-inheritedstatic"); - spoon.run(); - - CtClass classStatic = spoon.getFactory().Class().get(StaticImportWithInheritance.class); - CompilationUnit unitStatic = spoon.getFactory().CompilationUnit().getMap().get(classStatic.getPosition().getFile().getPath()); + CtClass classStatic = factory.Class().get(StaticImportWithInheritance.class); + CompilationUnit unitStatic = factory.CompilationUnit().getMap().get(classStatic.getPosition().getFile().getPath()); Collection imports = unitStatic.getImports(); assertEquals(1, imports.size()); @@ -182,18 +161,11 @@ public void testWithStaticInheritedImport() { assertEquals("import static spoon.test.jdtimportbuilder.testclasses.staticimport.DependencySubClass.*;", ctImport.toString()); } - @Test - public void testWithImportFromItf() { + @ModelTest(value = "./src/test/resources/jdtimportbuilder/", autoImport = true) + public void testWithImportFromItf(Factory factory) { // contract: When using starred static import of an interface, it imports a starred type - Launcher spoon = new Launcher(); - spoon.addInputResource("./src/test/resources/jdtimportbuilder/"); - spoon.getEnvironment().setAutoImports(true); - spoon.getEnvironment().setShouldCompile(true); - spoon.setSourceOutputDirectory("./target/spoon-jdtimport-itfimport"); - spoon.run(); - - CtClass classStatic = spoon.getFactory().Class().get("jdtimportbuilder.ItfImport"); - CompilationUnit unitStatic = spoon.getFactory().CompilationUnit().getMap().get(classStatic.getPosition().getFile().getPath()); + CtClass classStatic = factory.Class().get("jdtimportbuilder.ItfImport"); + CompilationUnit unitStatic = factory.CompilationUnit().getMap().get(classStatic.getPosition().getFile().getPath()); Collection imports = unitStatic.getImports(); assertEquals(1, imports.size(), imports.toString()); diff --git a/src/test/java/spoon/test/labels/TestLabels.java b/src/test/java/spoon/test/labels/TestLabels.java index 3cb17af2276..c9b87a3570c 100644 --- a/src/test/java/spoon/test/labels/TestLabels.java +++ b/src/test/java/spoon/test/labels/TestLabels.java @@ -28,8 +28,10 @@ import spoon.reflect.code.CtDo; import spoon.reflect.code.CtContinue; import spoon.reflect.declaration.CtMethod; +import spoon.reflect.factory.Factory; import spoon.reflect.visitor.filter.NamedElementFilter; import org.junit.jupiter.api.Test; +import spoon.testing.utils.ModelTest; import java.util.List; @@ -42,13 +44,10 @@ * Created by urli on 19/06/2017. */ public class TestLabels { - @Test - public void testLabelsAreDetected() { - Launcher launcher = new Launcher(); - launcher.addInputResource("./src/test/java/spoon/test/labels/testclasses/ManyLabels.java"); - launcher.buildModel(); - CtMethod mainMethod = launcher.getFactory().getModel().getElements(new NamedElementFilter<>(CtMethod.class,"main")).get(0); + @ModelTest("./src/test/java/spoon/test/labels/testclasses/ManyLabels.java") + public void testLabelsAreDetected(Factory factory) { + CtMethod mainMethod = factory.getModel().getElements(new NamedElementFilter<>(CtMethod.class,"main")).get(0); CtBlock body = mainMethod.getBody(); assertEquals(2, body.getStatements().size()); diff --git a/src/test/java/spoon/test/literal/LiteralTest.java b/src/test/java/spoon/test/literal/LiteralTest.java index 8bdedcf5bf2..4f39387898e 100644 --- a/src/test/java/spoon/test/literal/LiteralTest.java +++ b/src/test/java/spoon/test/literal/LiteralTest.java @@ -28,6 +28,7 @@ import spoon.reflect.factory.TypeFactory; import spoon.reflect.declaration.CtClass; import org.junit.jupiter.api.Test; +import spoon.testing.utils.ModelTest; import java.util.TreeSet; @@ -147,16 +148,9 @@ public void testFactoryLiternal() { assertEquals(factory.Type().stringType(), literal.getType()); } - @Test - public void testEscapedString() { - + @ModelTest(value = "./src/test/java/spoon/test/literal/testclasses/EscapedLiteral.java", autoImport = true) + public void testEscapedString(Launcher launcher) { /* test escaped char: spoon change octal values by equivalent unicode values */ - - Launcher launcher = new Launcher(); - launcher.addInputResource("./src/test/java/spoon/test/literal/testclasses/EscapedLiteral.java"); - launcher.getEnvironment().setCommentEnabled(true); - launcher.getEnvironment().setAutoImports(true); - launcher.buildModel(); final CtClass ctClass = launcher.getFactory().Class().get("spoon.test.literal.testclasses.EscapedLiteral"); assertTrue('\u0000' == (char) ((CtLiteral) ctClass.getField("c1").getDefaultExpression()).getValue()); @@ -182,13 +176,10 @@ public void testEscapedString() { assertTrue('\u0002' == (char) ((CtLiteral) ctClass.getField("c9").getDefaultExpression()).getValue()); } - @Test - public void testLiteralBase() { + @ModelTest("./src/test/java/spoon/test/literal/testclasses/BasedLiteral.java") + public void testLiteralBase(Factory factory) { // contract: CtLiteral should provide correct base (2, 8, 10, 16 or empty) - Launcher launcher = new Launcher(); - launcher.addInputResource("./src/test/java/spoon/test/literal/testclasses/BasedLiteral.java"); - launcher.buildModel(); - final CtClass ctClass = launcher.getFactory().Class().get("spoon.test.literal.testclasses.BasedLiteral"); + final CtClass ctClass = factory.Class().get("spoon.test.literal.testclasses.BasedLiteral"); assertEquals(LiteralBase.DECIMAL, ((CtLiteral) ctClass.getField("i1").getDefaultExpression()).getBase()); assertEquals(LiteralBase.DECIMAL, ((CtLiteral) ctClass.getField("i2").getDefaultExpression()).getBase()); @@ -223,15 +214,12 @@ public void testLiteralBase() { assertNull(((CtLiteral) ctClass.getField("c1").getDefaultExpression()).getBase()); assertNull(((CtLiteral) ctClass.getField("s1").getDefaultExpression()).getBase()); - } + } - @Test - public void testLiteralBasePrinter() { + @ModelTest("./src/test/java/spoon/test/literal/testclasses/BasedLiteral.java") + public void testLiteralBasePrinter(Factory factory) { // contract: PrettyPrinter should output literals in the specified base - Launcher launcher = new Launcher(); - launcher.addInputResource("./src/test/java/spoon/test/literal/testclasses/BasedLiteral.java"); - launcher.buildModel(); - final CtClass ctClass = launcher.getFactory().Class().get("spoon.test.literal.testclasses.BasedLiteral"); + final CtClass ctClass = factory.Class().get("spoon.test.literal.testclasses.BasedLiteral"); assertEquals("42", ctClass.getField("i1").getDefaultExpression().toString()); ((CtLiteral) ctClass.getField("i1").getDefaultExpression()).setBase(LiteralBase.OCTAL); diff --git a/src/test/java/spoon/test/parameters/ParameterTest.java b/src/test/java/spoon/test/parameters/ParameterTest.java index bb0fe2a58aa..be58adcc2fd 100644 --- a/src/test/java/spoon/test/parameters/ParameterTest.java +++ b/src/test/java/spoon/test/parameters/ParameterTest.java @@ -21,13 +21,16 @@ import org.junit.jupiter.api.Test; import spoon.Launcher; +import spoon.reflect.CtModel; import spoon.reflect.declaration.CtClass; import spoon.reflect.declaration.CtMethod; import spoon.reflect.declaration.CtParameter; +import spoon.reflect.factory.Factory; import spoon.reflect.reference.CtParameterReference; import spoon.reflect.reference.CtTypeReference; import spoon.reflect.visitor.filter.NamedElementFilter; import spoon.reflect.visitor.filter.TypeFilter; +import spoon.testing.utils.ModelTest; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -35,16 +38,10 @@ public class ParameterTest { - @Test - public void testParameterInNoClasspath() { - final Launcher launcher = new Launcher(); - launcher.addInputResource("./src/test/resources/parameter"); - launcher.setSourceOutputDirectory("./target/parameter"); - launcher.getEnvironment().setNoClasspath(true); - launcher.run(); - + @ModelTest("./src/test/resources/parameter") + public void testParameterInNoClasspath(final Launcher launcher) { final CtClass aClass = launcher.getFactory().Class().get("org.eclipse.draw2d.text.FlowUtilities"); - final CtParameter parameter = aClass.getElements(new NamedElementFilter<>(CtParameter.class,"font")).get(0); + final CtParameter parameter = aClass.getElements(new NamedElementFilter<>(CtParameter.class, "font")).get(0); assertEquals("font", parameter.getSimpleName()); assertNotNull(parameter.getType()); @@ -52,14 +49,9 @@ public void testParameterInNoClasspath() { assertEquals("org.eclipse.swt.graphics.Font font", parameter.toString()); } - @Test - public void testGetParameterReferenceInLambdaNoClasspath() { - Launcher launcher = new Launcher(); - launcher.addInputResource("./src/test/resources/noclasspath/Tacos.java"); - launcher.getEnvironment().setNoClasspath(true); - launcher.buildModel(); - - CtMethod ctMethod = launcher.getFactory().Type().get("Tacos").getMethodsByName("setStarRatings").get(0); + @ModelTest("./src/test/resources/noclasspath/Tacos.java") + public void testGetParameterReferenceInLambdaNoClasspath(Factory factory) { + CtMethod ctMethod = factory.Type().get("Tacos").getMethodsByName("setStarRatings").get(0); CtParameter ctParameter = ctMethod.getBody().getStatement(0).getElements(new TypeFilter(CtParameter.class) { @Override public boolean matches(CtParameter element) { @@ -81,40 +73,35 @@ public boolean matches(CtParameterReference element) { } } - @Test @SuppressWarnings("unchecked") - public void testMultiParameterLambdaTypeReference() { - Launcher launcher = new Launcher(); - launcher.addInputResource("./src/test/resources/noclasspath/lambdas/MultiParameterLambda.java"); - launcher.getEnvironment().setNoClasspath(true); - launcher.buildModel(); - + @ModelTest("./src/test/resources/noclasspath/lambdas/MultiParameterLambda.java") + public void testMultiParameterLambdaTypeReference(CtModel model, Factory factory) { List parameters; // test string parameters - parameters = launcher.getModel() + parameters = model .getElements(new NamedElementFilter<>(CtMethod.class,"stringLambda")) .get(0) .getElements(new TypeFilter<>(CtParameter.class)); assertEquals(2, parameters.size()); for (final CtParameter param : parameters) { CtTypeReference refType = param.getReference().getType(); - assertEquals(launcher.getFactory().Type().STRING, refType); + assertEquals(factory.Type().STRING, refType); } // test integer parameters - parameters = launcher.getModel() + parameters = model .getElements(new NamedElementFilter<>(CtMethod.class,"integerLambda")) .get(0) .getElements(new TypeFilter<>(CtParameter.class)); assertEquals(2, parameters.size()); for (final CtParameter param : parameters) { CtTypeReference refType = param.getReference().getType(); - assertEquals(launcher.getFactory().Type().INTEGER, refType); + assertEquals(factory.Type().INTEGER, refType); } // test unknown parameters - parameters = launcher.getModel() + parameters = model .getElements(new NamedElementFilter<>(CtMethod.class,"unknownLambda")) .get(0) .getElements(new TypeFilter<>(CtParameter.class)); @@ -126,13 +113,10 @@ public void testMultiParameterLambdaTypeReference() { } } - @Test - public void testGetParentAfterGetParameterReference() { + @ModelTest("./src/test/resources/parameter/ParameterResource.java") + public void testGetParentAfterGetParameterReference(CtModel model) { // contract: after getting a parameter reference, the parent of the parameter type reference should still be the parameter itself - Launcher spoon = new Launcher(); - spoon.addInputResource("./src/test/resources/parameter/ParameterResource.java"); - spoon.buildModel(); - CtParameter parameter = spoon.getModel().getRootPackage().getElements(new TypeFilter<>(CtParameter.class)).get(0); + CtParameter parameter = model.getRootPackage().getElements(new TypeFilter<>(CtParameter.class)).get(0); CtParameterReference pref = parameter.getReference(); assertEquals(parameter, parameter.getType().getParent()); } diff --git a/src/test/java/spoon/test/pattern/TypePatternTest.java b/src/test/java/spoon/test/pattern/TypePatternTest.java index 08a3145992a..e59f09ef368 100644 --- a/src/test/java/spoon/test/pattern/TypePatternTest.java +++ b/src/test/java/spoon/test/pattern/TypePatternTest.java @@ -15,13 +15,19 @@ import spoon.reflect.code.BinaryOperatorKind; import spoon.reflect.code.CtBinaryOperator; import spoon.reflect.code.CtTypePattern; +import spoon.reflect.declaration.CtType; +import spoon.reflect.factory.Factory; import spoon.reflect.visitor.filter.TypeFilter; import spoon.support.compiler.VirtualFile; import spoon.support.reflect.code.CtTypePatternImpl; +import spoon.testing.utils.ModelTest; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.MatcherAssert.assertThat; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; public class TypePatternTest { @@ -67,4 +73,13 @@ public void testValidateParent() { // setting something else as parent must fail assertThrows(SpoonException.class, () -> pattern.setParent(launcher.getFactory().createBlock())); } + + @ModelTest(value = "src/test/resources/patternmatching/InstanceofPatternMatch.java", complianceLevel = 16) + void testTypePatternSourcePosition(Factory factory) { + // contract: the source position of the CtTypePattern is equal to its CtLocalVariableDeclaration + CtType x = factory.Type().get("X"); + CtTypePattern typePattern = x.getElements(new TypeFilter<>(CtTypePattern.class)).get(0); + assertTrue(typePattern.getPosition().isValidPosition()); + assertThat(typePattern.getPosition(), equalTo(typePattern.getVariable().getPosition())); + } } diff --git a/src/test/java/spoon/test/pkg/PackageTest.java b/src/test/java/spoon/test/pkg/PackageTest.java index 652e292f9c6..0f94bf96dd4 100644 --- a/src/test/java/spoon/test/pkg/PackageTest.java +++ b/src/test/java/spoon/test/pkg/PackageTest.java @@ -189,40 +189,31 @@ public void testAnnotationInPackageInfoWhenTemplatesCompiled() throws Exception canBeBuilt("./target/spooned/packageAndTemplate/spoon/test/pkg/package-info.java", 8); } - @Test - public void testRenamePackageAndPrettyPrint() { - final Launcher spoon = new Launcher(); - spoon.addInputResource("./src/test/java/spoon/test/pkg/testclasses/Foo.java"); - spoon.buildModel(); - - CtPackage ctPackage = spoon.getModel().getElements(new NamedElementFilter<>(CtPackage.class, "spoon")).get(0); + @ModelTest("./src/test/java/spoon/test/pkg/testclasses/Foo.java") + public void testRenamePackageAndPrettyPrint(CtModel model, Launcher launcher, Factory factory) { + CtPackage ctPackage = model.getElements(new NamedElementFilter<>(CtPackage.class, "spoon")).get(0); ctPackage.setSimpleName("otherName"); - CtClass foo = spoon.getModel().getElements(new NamedElementFilter<>(CtClass.class, "Foo")).get(0); + CtClass foo = model.getElements(new NamedElementFilter<>(CtClass.class, "Foo")).get(0); assertEquals("otherName.test.pkg.testclasses.Foo", foo.getQualifiedName()); - PrettyPrinter prettyPrinter = new DefaultJavaPrettyPrinter(spoon.getEnvironment()); - prettyPrinter.calculate(spoon.getFactory().CompilationUnit().getOrCreate("./src/test/java/spoon/test/pkg/testclasses/Foo.java"), Collections.singletonList(foo)); + PrettyPrinter prettyPrinter = new DefaultJavaPrettyPrinter(launcher.getEnvironment()); + prettyPrinter.calculate(factory.CompilationUnit().getOrCreate("./src/test/java/spoon/test/pkg/testclasses/Foo.java"), Collections.singletonList(foo)); String result = prettyPrinter.getResult(); assertTrue(result.contains("package otherName.test.pkg.testclasses;")); } - @Test - public void testRenamePackageAndPrettyPrintNoclasspath() { - final Launcher spoon = new Launcher(); - spoon.addInputResource("./src/test/resources/noclasspath/app/Test.java"); - spoon.getEnvironment().setNoClasspath(true); - spoon.buildModel(); - - CtPackage ctPackage = spoon.getModel().getElements(new NamedElementFilter<>(CtPackage.class, "app")).get(0); + @ModelTest("./src/test/resources/noclasspath/app/Test.java") + public void testRenamePackageAndPrettyPrintNoclasspath(CtModel model, Launcher launcher, Factory factory) { + CtPackage ctPackage = model.getElements(new NamedElementFilter<>(CtPackage.class, "app")).get(0); ctPackage.setSimpleName("otherName"); - CtClass foo = spoon.getModel().getElements(new NamedElementFilter<>(CtClass.class, "Test")).get(0); + CtClass foo = model.getElements(new NamedElementFilter<>(CtClass.class, "Test")).get(0); assertEquals("otherName.Test", foo.getQualifiedName()); - PrettyPrinter prettyPrinter = new DefaultJavaPrettyPrinter(spoon.getEnvironment()); - prettyPrinter.calculate(spoon.getFactory().CompilationUnit().getOrCreate("./src/test/resources/noclasspath/app/Test.java"), Collections.singletonList(foo)); + PrettyPrinter prettyPrinter = new DefaultJavaPrettyPrinter(launcher.getEnvironment()); + prettyPrinter.calculate(factory.CompilationUnit().getOrCreate("./src/test/resources/noclasspath/app/Test.java"), Collections.singletonList(foo)); String result = prettyPrinter.getResult(); assertTrue(result.contains("package otherName;")); @@ -249,27 +240,17 @@ public void testRenamePackageAndPrettyPrintWithProcessor() throws Exception { } } - @Test - public void testRenameRootPackage() { - final Launcher spoon = new Launcher(); - spoon.addInputResource("./src/test/resources/noclasspath/app/Test.java"); - spoon.getEnvironment().setNoClasspath(true); - spoon.buildModel(); - - CtPackage rootPackage = spoon.getFactory().Package().getRootPackage(); + @ModelTest("./src/test/resources/noclasspath/app/Test.java") + public void testRenameRootPackage(Factory factory) { + CtPackage rootPackage = factory.Package().getRootPackage(); String rootPackageName = rootPackage.getSimpleName(); rootPackage.setSimpleName("test"); assertEquals(rootPackageName, rootPackage.getSimpleName()); } - @Test - public void testRenameRootPackageWithNullOrEmpty() { - final Launcher spoon = new Launcher(); - spoon.addInputResource("./src/test/resources/noclasspath/app/Test.java"); - spoon.getEnvironment().setNoClasspath(true); - spoon.buildModel(); - - CtPackage rootPackage = spoon.getFactory().Package().getRootPackage(); + @ModelTest("./src/test/resources/noclasspath/app/Test.java") + public void testRenameRootPackageWithNullOrEmpty(Factory factory) { + CtPackage rootPackage = factory.Package().getRootPackage(); String rootPackageName = rootPackage.getSimpleName(); assertEquals(CtPackage.TOP_LEVEL_PACKAGE_NAME, rootPackageName); @@ -330,14 +311,10 @@ public void testNoPackageAssumptionWithStarImportNoClasspath() { assertTrue(pkgRef.isImplicit()); } - @Test - public void testGetFQNSimple() { + @ModelTest("./src/test/java/spoon/test/pkg/testclasses/Foo.java") + public void testGetFQNSimple(Factory factory) { // contract: CtPackageReference simple name is also the fully qualified name of its referenced package - final Launcher spoon = new Launcher(); - spoon.addInputResource("./src/test/java/spoon/test/pkg/testclasses/Foo.java"); - spoon.buildModel(); - - CtClass fooClass = spoon.getFactory().Class().get(Foo.class); + CtClass fooClass = factory.Class().get(Foo.class); CtField field = fooClass.getField("fieldList"); CtPackageReference fieldPkg = field.getType().getPackage(); @@ -345,15 +322,10 @@ public void testGetFQNSimple() { assertEquals("java.util", fieldPkg.getQualifiedName()); } - @Test - public void testGetFQNInNoClassPath() { + @ModelTest("./src/test/resources/noclasspath/TorIntegration.java") + public void testGetFQNInNoClassPath(Factory factory) { // contract: CtPackageReference simple name is also the fully qualified name of its referenced package, even in noclasspath - final Launcher spoon = new Launcher(); - spoon.addInputResource("./src/test/resources/noclasspath/TorIntegration.java"); - spoon.getEnvironment().setNoClasspath(true); - spoon.buildModel(); - - CtClass torClass = spoon.getFactory().Class().get("com.duckduckgo.mobile.android.util.TorIntegration"); + CtClass torClass = factory.Class().get("com.duckduckgo.mobile.android.util.TorIntegration"); CtField field = torClass.getField("orbotHelper"); CtPackageReference fieldPkg = field.getType().getPackage(); diff --git a/src/test/java/spoon/test/prettyprinter/SniperAnnotatedEnumTest.java b/src/test/java/spoon/test/prettyprinter/SniperAnnotatedEnumTest.java new file mode 100644 index 00000000000..47a5d048b1f --- /dev/null +++ b/src/test/java/spoon/test/prettyprinter/SniperAnnotatedEnumTest.java @@ -0,0 +1,60 @@ +package spoon.test.prettyprinter; + +import static org.hamcrest.MatcherAssert.assertThat; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +import org.apache.commons.io.FileUtils; +import org.hamcrest.CoreMatchers; +import org.junit.jupiter.api.BeforeAll; + +import spoon.Launcher; +import spoon.compiler.Environment; +import spoon.reflect.CtModel; +import spoon.reflect.code.CtComment.CommentType; +import spoon.reflect.declaration.CtClass; +import spoon.reflect.visitor.filter.TypeFilter; +import spoon.support.sniper.SniperJavaPrettyPrinter; +import spoon.test.GitHubIssue; + +public class SniperAnnotatedEnumTest { + private static final Path INPUT_PATH = Paths.get("src/test/java/"); + private static final Path OUTPUT_PATH = Paths.get("target/test-output"); + + @BeforeAll + public static void setup() throws IOException { + FileUtils.deleteDirectory(OUTPUT_PATH.toFile()); + } + + @GitHubIssue(issueNumber = 4779, fixed = true) + public void annotatedEnumTest() throws IOException { + runSniperJavaPrettyPrinter("spoon/test/prettyprinter/testclasses/AnnotatedEnum.java"); + } + + private void runSniperJavaPrettyPrinter(String path) throws IOException { + final Launcher launcher = new Launcher(); + final Environment e = launcher.getEnvironment(); + e.setLevel("INFO"); + e.setPrettyPrinterCreator(() -> new SniperJavaPrettyPrinter(e)); + + launcher.addInputResource(INPUT_PATH.resolve(path).toString()); + launcher.setSourceOutputDirectory(OUTPUT_PATH.toString()); + + CtModel model = launcher.buildModel(); + CtClass ctClass = model.getElements(new TypeFilter<>(CtClass.class)).get(0); + + ctClass.addComment(launcher.getFactory().Code().createComment("test", CommentType.BLOCK)); + + launcher.process(); + launcher.prettyprint(); + // Verify result file exists and is not empty + assertThat("Output file for " + path + " should exist", OUTPUT_PATH.resolve(path).toFile().exists(), + CoreMatchers.equalTo(true)); + + String content = Files.readString(OUTPUT_PATH.resolve(path)); + assertThat(content.trim(), CoreMatchers.containsString("/* test */public enum")); + } +} diff --git a/src/test/java/spoon/test/prettyprinter/SniperAssertTest.java b/src/test/java/spoon/test/prettyprinter/SniperAssertTest.java index 7ec43589ebc..ec3b7db9645 100644 --- a/src/test/java/spoon/test/prettyprinter/SniperAssertTest.java +++ b/src/test/java/spoon/test/prettyprinter/SniperAssertTest.java @@ -1,27 +1,30 @@ package spoon.test.prettyprinter; -import org.apache.commons.io.*; -import org.hamcrest.*; -import org.junit.*; -import static org.junit.Assert.*; +import static org.hamcrest.MatcherAssert.assertThat; -import spoon.Launcher; -import spoon.compiler.*; -import spoon.reflect.*; -import spoon.reflect.code.CtComment.*; -import spoon.reflect.declaration.*; -import spoon.reflect.visitor.filter.*; -import spoon.support.sniper.*; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +import org.apache.commons.io.FileUtils; +import org.hamcrest.CoreMatchers; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; -import java.io.*; -import java.nio.charset.*; -import java.nio.file.*; +import spoon.Launcher; +import spoon.compiler.Environment; +import spoon.reflect.CtModel; +import spoon.reflect.code.CtComment.CommentType; +import spoon.reflect.declaration.CtMethod; +import spoon.reflect.visitor.filter.TypeFilter; +import spoon.support.sniper.SniperJavaPrettyPrinter; public class SniperAssertTest { private static final Path INPUT_PATH = Paths.get("src/test/java/"); private static final Path OUTPUT_PATH = Paths.get("target/test-output"); - @BeforeClass + @BeforeAll public static void setup() throws IOException { FileUtils.deleteDirectory(OUTPUT_PATH.toFile()); } @@ -56,7 +59,7 @@ private void runSniperJavaPrettyPrinter(String path) throws IOException { assertThat("Output file for " + path + " should exist", OUTPUT_PATH.resolve(path).toFile().exists(), CoreMatchers.equalTo(true)); - String content = new String(Files.readAllBytes(OUTPUT_PATH.resolve(path)), StandardCharsets.UTF_8); + String content = Files.readString(OUTPUT_PATH.resolve(path)); assertThat(content, CoreMatchers.notNullValue()); assertThat("Result class should not be empty", content.trim(), CoreMatchers.not(CoreMatchers.equalTo(""))); diff --git a/src/test/java/spoon/test/prettyprinter/SniperDefaultMethodTest.java b/src/test/java/spoon/test/prettyprinter/SniperDefaultMethodTest.java index 08dbc7ed141..35d2e0dac46 100644 --- a/src/test/java/spoon/test/prettyprinter/SniperDefaultMethodTest.java +++ b/src/test/java/spoon/test/prettyprinter/SniperDefaultMethodTest.java @@ -1,27 +1,30 @@ package spoon.test.prettyprinter; -import org.apache.commons.io.*; -import org.hamcrest.*; -import org.junit.*; -import static org.junit.Assert.*; +import static org.hamcrest.MatcherAssert.assertThat; -import spoon.Launcher; -import spoon.compiler.*; -import spoon.reflect.*; -import spoon.reflect.code.CtComment.*; -import spoon.reflect.declaration.*; -import spoon.reflect.visitor.filter.*; -import spoon.support.sniper.*; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +import org.apache.commons.io.FileUtils; +import org.hamcrest.CoreMatchers; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; -import java.io.*; -import java.nio.charset.*; -import java.nio.file.*; +import spoon.Launcher; +import spoon.compiler.Environment; +import spoon.reflect.CtModel; +import spoon.reflect.code.CtComment.CommentType; +import spoon.reflect.declaration.CtMethod; +import spoon.reflect.visitor.filter.TypeFilter; +import spoon.support.sniper.SniperJavaPrettyPrinter; public class SniperDefaultMethodTest { private static final Path INPUT_PATH = Paths.get("src/test/java/"); private static final Path OUTPUT_PATH = Paths.get("target/test-output"); - @BeforeClass + @BeforeAll public static void setup() throws IOException { FileUtils.deleteDirectory(OUTPUT_PATH.toFile()); } @@ -52,7 +55,7 @@ private void runSniperJavaPrettyPrinter(String path) throws IOException { assertThat("Output file for " + path + " should exist", OUTPUT_PATH.resolve(path).toFile().exists(), CoreMatchers.equalTo(true)); - String content = new String(Files.readAllBytes(OUTPUT_PATH.resolve(path)), StandardCharsets.UTF_8); + String content = Files.readString(OUTPUT_PATH.resolve(path)); assertThat(content, CoreMatchers.notNullValue()); assertThat("Result class should not be empty", content.trim(), CoreMatchers.not(CoreMatchers.equalTo(""))); diff --git a/src/test/java/spoon/test/prettyprinter/SniperDoubleBoundTest.java b/src/test/java/spoon/test/prettyprinter/SniperDoubleBoundTest.java index f5838544ecb..df343a35812 100644 --- a/src/test/java/spoon/test/prettyprinter/SniperDoubleBoundTest.java +++ b/src/test/java/spoon/test/prettyprinter/SniperDoubleBoundTest.java @@ -1,27 +1,30 @@ package spoon.test.prettyprinter; -import org.apache.commons.io.*; -import org.hamcrest.*; -import org.junit.*; -import static org.junit.Assert.*; +import static org.hamcrest.MatcherAssert.assertThat; -import spoon.Launcher; -import spoon.compiler.*; -import spoon.reflect.*; -import spoon.reflect.code.CtComment.*; -import spoon.reflect.declaration.*; -import spoon.reflect.visitor.filter.*; -import spoon.support.sniper.*; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +import org.apache.commons.io.FileUtils; +import org.hamcrest.CoreMatchers; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; -import java.io.*; -import java.nio.charset.*; -import java.nio.file.*; +import spoon.Launcher; +import spoon.compiler.Environment; +import spoon.reflect.CtModel; +import spoon.reflect.code.CtComment.CommentType; +import spoon.reflect.declaration.CtClass; +import spoon.reflect.visitor.filter.TypeFilter; +import spoon.support.sniper.SniperJavaPrettyPrinter; public class SniperDoubleBoundTest { private static final Path INPUT_PATH = Paths.get("src/test/java/"); private static final Path OUTPUT_PATH = Paths.get("target/test-output"); - @BeforeClass + @BeforeAll public static void setup() throws IOException { FileUtils.deleteDirectory(OUTPUT_PATH.toFile()); } @@ -51,7 +54,7 @@ private void runSniperJavaPrettyPrinter(String path) throws IOException { assertThat("Output file for " + path + " should exist", OUTPUT_PATH.resolve(path).toFile().exists(), CoreMatchers.equalTo(true)); - String content = new String(Files.readAllBytes(OUTPUT_PATH.resolve(path)), StandardCharsets.UTF_8); + String content = Files.readString(OUTPUT_PATH.resolve(path)); assertThat(content.trim(), CoreMatchers.containsString("/* test */public class DoubleBound")); } } diff --git a/src/test/java/spoon/test/prettyprinter/SniperDoubleForInitializerTest.java b/src/test/java/spoon/test/prettyprinter/SniperDoubleForInitializerTest.java index 3bdfd08c161..9e609706b88 100644 --- a/src/test/java/spoon/test/prettyprinter/SniperDoubleForInitializerTest.java +++ b/src/test/java/spoon/test/prettyprinter/SniperDoubleForInitializerTest.java @@ -1,27 +1,29 @@ package spoon.test.prettyprinter; +import static org.hamcrest.MatcherAssert.assertThat; -import org.apache.commons.io.*; -import org.hamcrest.*; -import org.junit.*; -import static org.junit.Assert.*; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; -import spoon.Launcher; -import spoon.compiler.*; -import spoon.reflect.*; -import spoon.reflect.code.CtComment.*; -import spoon.reflect.declaration.*; -import spoon.reflect.visitor.filter.*; -import spoon.support.sniper.*; +import org.apache.commons.io.FileUtils; +import org.hamcrest.CoreMatchers; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; -import java.io.*; -import java.nio.charset.*; -import java.nio.file.*; +import spoon.Launcher; +import spoon.compiler.Environment; +import spoon.reflect.CtModel; +import spoon.reflect.code.CtComment.CommentType; +import spoon.reflect.declaration.CtMethod; +import spoon.reflect.visitor.filter.TypeFilter; +import spoon.support.sniper.SniperJavaPrettyPrinter; public class SniperDoubleForInitializerTest { private static final Path INPUT_PATH = Paths.get("src/test/java/"); private static final Path OUTPUT_PATH = Paths.get("target/test-output"); - @BeforeClass + @BeforeAll public static void setup() throws IOException { FileUtils.deleteDirectory(OUTPUT_PATH.toFile()); } @@ -52,7 +54,7 @@ private void runSniperJavaPrettyPrinter(String path) throws IOException { assertThat("Output file for " + path + " should exist", OUTPUT_PATH.resolve(path).toFile().exists(), CoreMatchers.equalTo(true)); - String content = new String(Files.readAllBytes(OUTPUT_PATH.resolve(path)), StandardCharsets.UTF_8); + String content = Files.readString(OUTPUT_PATH.resolve(path)); assertThat(content, CoreMatchers.notNullValue()); assertThat("Result class should not be empty", content.trim(), CoreMatchers.not(CoreMatchers.equalTo(""))); diff --git a/src/test/java/spoon/test/prettyprinter/SniperInnerTypeTest.java b/src/test/java/spoon/test/prettyprinter/SniperInnerTypeTest.java index be59dac9a99..6b4abbcdd6d 100644 --- a/src/test/java/spoon/test/prettyprinter/SniperInnerTypeTest.java +++ b/src/test/java/spoon/test/prettyprinter/SniperInnerTypeTest.java @@ -1,27 +1,30 @@ package spoon.test.prettyprinter; -import org.apache.commons.io.*; -import org.hamcrest.*; -import org.junit.*; -import static org.junit.Assert.*; +import static org.hamcrest.MatcherAssert.assertThat; -import spoon.Launcher; -import spoon.compiler.*; -import spoon.reflect.*; -import spoon.reflect.code.CtComment.*; -import spoon.reflect.declaration.*; -import spoon.reflect.visitor.filter.*; -import spoon.support.sniper.*; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +import org.apache.commons.io.FileUtils; +import org.hamcrest.CoreMatchers; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; -import java.io.*; -import java.nio.charset.*; -import java.nio.file.*; +import spoon.Launcher; +import spoon.compiler.Environment; +import spoon.reflect.CtModel; +import spoon.reflect.code.CtComment.CommentType; +import spoon.reflect.declaration.CtClass; +import spoon.reflect.visitor.filter.TypeFilter; +import spoon.support.sniper.SniperJavaPrettyPrinter; public class SniperInnerTypeTest { private static final Path INPUT_PATH = Paths.get("src/test/java/"); private static final Path OUTPUT_PATH = Paths.get("target/test-output"); - @BeforeClass + @BeforeAll public static void setup() throws IOException { FileUtils.deleteDirectory(OUTPUT_PATH.toFile()); } @@ -62,7 +65,7 @@ private void runSniperJavaPrettyPrinter(String path) throws IOException { assertThat("Output file for " + path + " should exist", OUTPUT_PATH.resolve(path).toFile().exists(), CoreMatchers.equalTo(true)); - String content = new String(Files.readAllBytes(OUTPUT_PATH.resolve(path)), StandardCharsets.UTF_8); + String content = Files.readString(OUTPUT_PATH.resolve(path)); assertThat(content, CoreMatchers.notNullValue()); assertThat("Result class should not be empty", content.trim(), CoreMatchers.not(CoreMatchers.equalTo(""))); diff --git a/src/test/java/spoon/test/prettyprinter/TestSniperPrinter.java b/src/test/java/spoon/test/prettyprinter/TestSniperPrinter.java index 1bd101df15e..b00cf7b0d4b 100644 --- a/src/test/java/spoon/test/prettyprinter/TestSniperPrinter.java +++ b/src/test/java/spoon/test/prettyprinter/TestSniperPrinter.java @@ -812,7 +812,7 @@ void testRoundBracketPrintingInComplexArithmeticExpression() { testSniper("ArithmeticExpression", noOpModifyFieldAssignment, assertPrintsRoundBracketsCorrectly); } - @GitHubIssue(issueNumber = 4218, fixed = false) + @GitHubIssue(issueNumber = 4218, fixed = true) void testSniperDoesNotPrintTheDeletedAnnotation() { Consumer> deleteAnnotation = type -> { type.getAnnotations().forEach(CtAnnotation::delete); @@ -1079,15 +1079,37 @@ public void testToStringWithSniperPrinter(String inputSourcePath) throws Excepti public void testToStringWithSniperOnElementScan() throws Exception { testToStringWithSniperPrinter("src/test/java/spoon/test/prettyprinter/testclasses/ElementScan.java"); } - - /** - * Files to be tested with testNoChangeDiff - */ - private static Stream noChangeDiffTestFiles() { - Path path = Paths.get("src/test/java/spoon/test/prettyprinter/testclasses/difftest"); - return FileUtils.listFiles(path.toFile(), null, false).stream(); + @GitHubIssue(issueNumber = 3811, fixed = true) + void noChangeDiffBrackets() throws IOException { + testNoChangeDiffFailing( + Paths.get("src/test/java/spoon/test/prettyprinter/testclasses/difftest/Brackets").toFile()); + } + @GitHubIssue(issueNumber = 3811, fixed = true) + void noChangeDiffConditionalComment() throws IOException { + testNoChangeDiffFailing( + Paths.get("src/test/java/spoon/test/prettyprinter/testclasses/difftest/ConditionalComment").toFile()); } + @GitHubIssue(issueNumber = 3811, fixed = true) + void noChangeDiffEnumComment() throws IOException { + testNoChangeDiffFailing( + Paths.get("src/test/java/spoon/test/prettyprinter/testclasses/difftest/EnumComment").toFile()); + } + @GitHubIssue(issueNumber = 3811, fixed = true) + void noChangeDiffEnumTest() throws IOException { + testNoChangeDiffFailing( + Paths.get("src/test/java/spoon/test/prettyprinter/testclasses/difftest/EnumTest").toFile()); + } + @GitHubIssue(issueNumber = 3811, fixed = true) + void noChangeDiffExceptionTest() throws IOException { + testNoChangeDiffFailing( + Paths.get("src/test/java/spoon/test/prettyprinter/testclasses/difftest/ExceptionTest").toFile()); + } + @GitHubIssue(issueNumber = 3811, fixed = true) + void noChangeDiffMethodComment() throws IOException { + testNoChangeDiffFailing( + Paths.get("src/test/java/spoon/test/prettyprinter/testclasses/difftest/MethodComment").toFile()); + } /** * Test various syntax by doing an change to every element that should not * result in any change in source. This forces the sniper printer to recreate @@ -1095,10 +1117,7 @@ private static Stream noChangeDiffTestFiles() { * * Reference: #3811 */ - @ParameterizedTest - @MethodSource("noChangeDiffTestFiles") - @GitHubIssue(issueNumber = 3811, fixed = false) - public void testNoChangeDiff(File file) throws IOException { + private void testNoChangeDiffFailing(File file) throws IOException { String fileName = file.getName(); Path outputPath = Paths.get("target/test-output"); File outputFile = outputPath.resolve("spoon/test/prettyprinter/testclasses/difftest") diff --git a/src/test/java/spoon/test/prettyprinter/testclasses/AnnotatedEnum.java b/src/test/java/spoon/test/prettyprinter/testclasses/AnnotatedEnum.java new file mode 100644 index 00000000000..fac68044185 --- /dev/null +++ b/src/test/java/spoon/test/prettyprinter/testclasses/AnnotatedEnum.java @@ -0,0 +1,22 @@ +package spoon.test.prettyprinter.testclasses; + +public enum AnnotatedEnum { + ONE(1, "one"), + TWO(2, "two"), + THREE(3, "three"), + /** + * @deprecated + */ + @Deprecated + // There was a typo... + FOR(4, "for"), + FOUR(4, "four"); + + private final int value; + private final String text; + + AnnotatedEnum(int value, String text) { + this.value = value; + this.text = text; + } +} diff --git a/src/test/java/spoon/test/sealedclasses/SealedClassesTest.java b/src/test/java/spoon/test/sealedclasses/SealedClassesTest.java new file mode 100644 index 00000000000..39883708c86 --- /dev/null +++ b/src/test/java/spoon/test/sealedclasses/SealedClassesTest.java @@ -0,0 +1,106 @@ +package spoon.test.sealedclasses; + +import org.junit.jupiter.api.Test; +import spoon.Launcher; +import spoon.reflect.CtModel; +import spoon.reflect.declaration.CtClass; +import spoon.reflect.declaration.CtEnum; +import spoon.reflect.declaration.CtPackage; +import spoon.reflect.declaration.CtSealable; +import spoon.reflect.declaration.CtType; +import spoon.reflect.declaration.ModifierKind; +import spoon.reflect.reference.CtTypeReference; +import spoon.reflect.visitor.filter.TypeFilter; +import spoon.support.reflect.CtExtendedModifier; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.IsCollectionContaining.hasItem; +import static org.hamcrest.core.IsCollectionContaining.hasItems; +import static spoon.test.SpoonTestHelpers.contentEquals; + +public class SealedClassesTest { + + @Test + void testSealedClassWithInnerSubclassModelImplicit() { + // contract: inner subtypes are implicit and in the permitted types set + Launcher launcher = createLauncher(); + + launcher.addInputResource("src/test/resources/sealedclasses/SealedClassWithNestedSubclasses.java"); + CtModel ctModel = launcher.buildModel(); + CtSealable outer = (CtSealable) ctModel.getAllTypes().iterator().next(); + + for (CtTypeReference permitted : outer.getPermittedTypes()) { + assertThat(permitted.isImplicit(), is(true)); + } + } + + @Test + void testSealedClassWithInnerSubclassModelModifiers() { + // contract: sealed and non-sealed modifiers are present in the model + Launcher launcher = createLauncher(); + + launcher.addInputResource("src/test/resources/sealedclasses/SealedClassWithNestedSubclasses.java"); + CtModel ctModel = launcher.buildModel(); + CtClass outer = (CtClass) ctModel.getAllTypes().iterator().next(); + assertThat(outer.getExtendedModifiers(), hasItems(new CtExtendedModifier(ModifierKind.SEALED, false))); + + CtClass nestedFinal = outer.getNestedType("NestedFinal"); + assertThat(nestedFinal.getExtendedModifiers(), hasItem(new CtExtendedModifier(ModifierKind.FINAL, false))); + assertThat(outer.getPermittedTypes(), hasItem(nestedFinal.getReference())); + + CtType nestedNonSealed = outer.getNestedType("NestedNonSealed"); + assertThat(nestedNonSealed.getExtendedModifiers(), hasItem(new CtExtendedModifier(ModifierKind.NON_SEALED, false))); + assertThat(outer.getPermittedTypes(), hasItem(nestedNonSealed.getReference())); + } + + @Test + void testEnumSealed() { + // contract: enums with anonymous enum values are sealed and the anonymous types are final + Launcher launcher = createLauncher(); + + launcher.addInputResource("src/test/resources/sealedclasses/EnumWithAnonymousValue.java"); + CtModel ctModel = launcher.buildModel(); + CtEnum ctEnum = ctModel.getElements(new TypeFilter>(CtEnum.class)).get(0); + // not final + assertThat(ctEnum.isFinal(), is(false)); + // but (implicitly) sealed + assertThat(ctEnum.getExtendedModifiers(), contentEquals( + new CtExtendedModifier(ModifierKind.PUBLIC, false), + new CtExtendedModifier(ModifierKind.SEALED, true) + )); + + // TODO the RHS type is wrong currently, see #4291 +/* assertThat(ctEnum.getPermittedTypes(), + contentEquals(ctEnum.getEnumValue("VALUE").getDefaultExpression().getType()));*/ + } + + @Test + void testMultiCompilationUnitSealed() { + // contract: extending types in other compilation units are present in the permitted types set + Launcher launcher = createLauncher(); + + launcher.addInputResource("src/test/resources/sealedclasses/SealedClassWithPermits.java"); + launcher.addInputResource("src/test/resources/sealedclasses/ExtendingClass.java"); + launcher.addInputResource("src/test/resources/sealedclasses/OtherExtendingClass.java"); + CtModel ctModel = launcher.buildModel(); + CtPackage ctPackage = ctModel.getAllPackages().iterator().next(); + CtClass sealedClassWithPermits = ctPackage.getType("SealedClassWithPermits"); + CtType extendingClass = ctPackage.getType("ExtendingClass"); + CtType otherExtendingClass = ctPackage.getType("OtherExtendingClass"); + + assertThat(sealedClassWithPermits.getPermittedTypes().size(), is(2)); + for (CtTypeReference permittedType : sealedClassWithPermits.getPermittedTypes()) { + // outer types are always explicit + assertThat(permittedType.isImplicit(), is(false)); + } + assertThat(sealedClassWithPermits.getPermittedTypes(), hasItem(extendingClass.getReference())); + assertThat(sealedClassWithPermits.getPermittedTypes(), hasItem(otherExtendingClass.getReference())); + } + + private static Launcher createLauncher() { + Launcher launcher = new Launcher(); + launcher.getEnvironment().setComplianceLevel(17); + return launcher; + } +} diff --git a/src/test/java/spoon/test/trycatch/TryCatchTest.java b/src/test/java/spoon/test/trycatch/TryCatchTest.java index 48d0d5a4417..8f1ed9b6368 100644 --- a/src/test/java/spoon/test/trycatch/TryCatchTest.java +++ b/src/test/java/spoon/test/trycatch/TryCatchTest.java @@ -50,6 +50,7 @@ import spoon.support.reflect.CtExtendedModifier; import spoon.test.trycatch.testclasses.Foo; import spoon.test.trycatch.testclasses.Main; +import spoon.testing.utils.ModelTest; import spoon.testing.utils.LineSeperatorExtension; @@ -395,8 +396,8 @@ public void testCatchWithQualifiedAndUnqualifiedTypeReferencesInSameCatcher() th assertFalse(paramTypes.get(1).isSimplyQualified(), "second type reference should be qualified"); } - @Test - public void testNonCloseableGenericTypeInTryWithResources() { + @ModelTest("src/test/resources/NonClosableGenericInTryWithResources.java") + public void testNonCloseableGenericTypeInTryWithResources(CtModel model) { // contract: When a non-closeable generic type is used in a try-with-resources, it's type // becomes a problem type in JDT, with the type parameters included in the compound name. // This is as opposed to a parameterized type, so we need to take special care in parsing @@ -406,10 +407,6 @@ public void testNonCloseableGenericTypeInTryWithResources() { // // This previously caused a crash, see https://github.com/INRIA/spoon/issues/3951 - final Launcher launcher = new Launcher(); - launcher.addInputResource("src/test/resources/NonClosableGenericInTryWithResources.java"); - - CtModel model = launcher.buildModel(); CtLocalVariableReference varRef = model.filterChildren(CtLocalVariableReference.class::isInstance).first(); assertThat(varRef.getType().getQualifiedName(), equalTo("NonClosableGenericInTryWithResources.GenericType")); diff --git a/src/test/java/spoon/test/visitor/AssignmentsEqualsTest.java b/src/test/java/spoon/test/visitor/AssignmentsEqualsTest.java index cd760eb83dd..2791982995a 100644 --- a/src/test/java/spoon/test/visitor/AssignmentsEqualsTest.java +++ b/src/test/java/spoon/test/visitor/AssignmentsEqualsTest.java @@ -8,19 +8,15 @@ import spoon.reflect.factory.Factory; import spoon.reflect.visitor.Query; import spoon.reflect.visitor.filter.TypeFilter; +import spoon.testing.utils.ModelTest; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotEquals; public class AssignmentsEqualsTest { - @Test - public void testEquals() { - Launcher launcher = new Launcher(); - launcher.addInputResource("./src/test/resources/spoon/test/visitor/Assignments.java"); - launcher.buildModel(); - - Factory factory = launcher.getFactory(); + @ModelTest("./src/test/resources/spoon/test/visitor/Assignments.java") + public void testEquals(Factory factory) { List assignments = Query.getElements(factory, new TypeFilter<>(CtAssignment.class)); assertEquals(assignments.size(), 10); assertNotEquals(assignments.get(0), assignments.get(1)); diff --git a/src/test/java/spoon/test/visitor/VisitorTest.java b/src/test/java/spoon/test/visitor/VisitorTest.java index 5f22894de40..acbe2198181 100644 --- a/src/test/java/spoon/test/visitor/VisitorTest.java +++ b/src/test/java/spoon/test/visitor/VisitorTest.java @@ -4,7 +4,9 @@ import spoon.Launcher; import spoon.reflect.code.CtIf; import spoon.reflect.declaration.CtMethod; +import spoon.reflect.factory.Factory; import spoon.reflect.visitor.CtScanner; +import spoon.testing.utils.ModelTest; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -36,14 +38,10 @@ public void visitCtIf(CtIf ifElement) { } } - @Test - public void testRecursiveDescent() { - Launcher launcher = new Launcher(); - launcher.addInputResource("./src/test/resources/spoon/test/visitor/Foo.java"); - launcher.buildModel(); - + @ModelTest("./src/test/resources/spoon/test/visitor/Foo.java") + public void testRecursiveDescent(Factory factory) { final MyVisitor visitor = new MyVisitor(2); - visitor.scan(launcher.getFactory().Package().getRootPackage()); + visitor.scan(factory.Package().getRootPackage()); assertTrue(visitor.equals); } } diff --git a/src/test/resources/patternmatching/InstanceofPatternMatch.java b/src/test/resources/patternmatching/InstanceofPatternMatch.java new file mode 100644 index 00000000000..61b08e23fa2 --- /dev/null +++ b/src/test/resources/patternmatching/InstanceofPatternMatch.java @@ -0,0 +1,8 @@ +class X { + String typePattern(Object obj) { + if (obj instanceof String s) { + return s; + } + return ""; + } +} diff --git a/src/test/resources/sealedclasses/EnumWithAnonymousValue.java b/src/test/resources/sealedclasses/EnumWithAnonymousValue.java new file mode 100644 index 00000000000..c9954af472c --- /dev/null +++ b/src/test/resources/sealedclasses/EnumWithAnonymousValue.java @@ -0,0 +1,5 @@ +public enum EnumWithAnonymousValue { + VALUE { + + } +} \ No newline at end of file diff --git a/src/test/resources/sealedclasses/ExtendingClass.java b/src/test/resources/sealedclasses/ExtendingClass.java new file mode 100644 index 00000000000..ad0ef2cbf9f --- /dev/null +++ b/src/test/resources/sealedclasses/ExtendingClass.java @@ -0,0 +1,3 @@ +public final class ExtendingClass extends SealedClassWithPermits implements SealedInterfaceWithPermits { + +} diff --git a/src/test/resources/sealedclasses/OtherExtendingClass.java b/src/test/resources/sealedclasses/OtherExtendingClass.java new file mode 100644 index 00000000000..9e9d3f0afcf --- /dev/null +++ b/src/test/resources/sealedclasses/OtherExtendingClass.java @@ -0,0 +1,2 @@ +public final class OtherExtendingClass extends SealedClassWithPermits implements SealedInterfaceWithPermits { +} diff --git a/src/test/resources/sealedclasses/SealedClassWithNestedSubclasses.java b/src/test/resources/sealedclasses/SealedClassWithNestedSubclasses.java new file mode 100644 index 00000000000..e094c1c4a35 --- /dev/null +++ b/src/test/resources/sealedclasses/SealedClassWithNestedSubclasses.java @@ -0,0 +1,10 @@ +public sealed class SealedClassWithNestedSubclasses { + + public static final class NestedFinal extends SealedClassWithNestedSubclasses { + + } + + public non-sealed static class NestedNonSealed extends SealedClassWithNestedSubclasses { + + } +} \ No newline at end of file diff --git a/src/test/resources/sealedclasses/SealedClassWithPermits.java b/src/test/resources/sealedclasses/SealedClassWithPermits.java new file mode 100644 index 00000000000..6e5c0038e4e --- /dev/null +++ b/src/test/resources/sealedclasses/SealedClassWithPermits.java @@ -0,0 +1,3 @@ +public sealed class SealedClassWithPermits + permits ExtendingClass, OtherExtendingClass { +} \ No newline at end of file diff --git a/src/test/resources/sealedclasses/SealedInterfaceWithNestedSubclasses.java b/src/test/resources/sealedclasses/SealedInterfaceWithNestedSubclasses.java new file mode 100644 index 00000000000..ebaebf61f47 --- /dev/null +++ b/src/test/resources/sealedclasses/SealedInterfaceWithNestedSubclasses.java @@ -0,0 +1,10 @@ +public sealed interface SealedInterfaceWithNestedSubclasses { + + final class NestedFinal extends SealedInterfaceWithNestedSubclasses { + + } + + non-sealed class NestedNonSealed extends SealedInterfaceWithNestedSubclasses { + + } +} diff --git a/src/test/resources/sealedclasses/SealedInterfaceWithPermits.java b/src/test/resources/sealedclasses/SealedInterfaceWithPermits.java new file mode 100644 index 00000000000..7510b69a8e8 --- /dev/null +++ b/src/test/resources/sealedclasses/SealedInterfaceWithPermits.java @@ -0,0 +1,3 @@ +public sealed interface SealedInterfaceWithPermits + permits ExtendingClass, OtherExtendingClass { +}