diff --git a/.gitattributes b/.gitattributes index 00a51aff..06baec4b 100644 --- a/.gitattributes +++ b/.gitattributes @@ -4,3 +4,5 @@ # These are explicitly windows files and should use crlf *.bat text eol=crlf +*.sh text eol=lf +gradlew text eol=lf \ No newline at end of file diff --git a/src/main/java/org/checkerframework/specimin/StandardTypeRuleDependencyMap.java b/src/main/java/org/checkerframework/specimin/StandardTypeRuleDependencyMap.java index b68e480d..13b159e2 100644 --- a/src/main/java/org/checkerframework/specimin/StandardTypeRuleDependencyMap.java +++ b/src/main/java/org/checkerframework/specimin/StandardTypeRuleDependencyMap.java @@ -140,6 +140,9 @@ public List getRelevantElements(Node node) { if (node instanceof NodeWithExtends withExtends) { elements.addAll(withExtends.getExtendedTypes()); } + if (node instanceof ClassOrInterfaceDeclaration decl) { + elements.addAll(decl.getPermittedTypes()); + } if (node instanceof TypeDeclaration typeDeclaration) { List mustImplement = JavaParserUtil.getDeclarationsForAllMustImplementMethods( diff --git a/src/main/java/org/checkerframework/specimin/unsolved/Sealedness.java b/src/main/java/org/checkerframework/specimin/unsolved/Sealedness.java new file mode 100644 index 00000000..8aa5aca6 --- /dev/null +++ b/src/main/java/org/checkerframework/specimin/unsolved/Sealedness.java @@ -0,0 +1,18 @@ +package org.checkerframework.specimin.unsolved; + +/** + * The "sealedness" of a synthetic type; i.e., the keyword that goes in front of the class + * declaration. + * + *

For example, {@link Sealedness#NON_SEALED} corresponds with {@code non-sealed class Foo}. + */ +public enum Sealedness { + /** No sealedness --> {@code class Foo} */ + NONE, + /** Non-sealed --> {@code non-sealed class Foo} */ + NON_SEALED, + /** Sealed --> {@code sealed class Foo} */ + SEALED, + /** Final --> {@code final class Foo} */ + FINAL +} diff --git a/src/main/java/org/checkerframework/specimin/unsolved/UnsolvedClassOrInterface.java b/src/main/java/org/checkerframework/specimin/unsolved/UnsolvedClassOrInterface.java index fd9234d0..fdc4f736 100644 --- a/src/main/java/org/checkerframework/specimin/unsolved/UnsolvedClassOrInterface.java +++ b/src/main/java/org/checkerframework/specimin/unsolved/UnsolvedClassOrInterface.java @@ -42,6 +42,9 @@ public class UnsolvedClassOrInterface extends UnsolvedSymbolAlternate /** The type of this type; i.e., is it a class, interface, annotation, enum? */ private UnsolvedClassOrInterfaceType typeOfType = UnsolvedClassOrInterfaceType.UNKNOWN; + /** The sealedness of this type; i.e., final, sealed, non-sealed, or none of these. */ + private Sealedness sealedness = Sealedness.NONE; + /** * This constructor correctly splits apart the class name and any generics attached to it. * @@ -141,6 +144,24 @@ public boolean doesExtend(MemberType extendsType) { return this.extendsClause != null && this.extendsClause.equals(extendsType); } + /** + * Sets the sealedness of this class. + * + * @param sealedness The sealedness + */ + public void setSealedness(Sealedness sealedness) { + this.sealedness = sealedness; + } + + /** + * Gets the sealedness of this class. + * + * @return The sealedness + */ + public Sealedness getSealedness() { + return sealedness; + } + /** * Adds an annotation to this class. * @@ -181,6 +202,7 @@ public UnsolvedClassOrInterface copy() { copy.typeOfType = this.typeOfType; copy.typeVariables = new ArrayList<>(this.typeVariables); copy.annotations = new HashSet<>(this.annotations); + copy.sealedness = this.sealedness; return copy; } @@ -228,6 +250,22 @@ public String toString( // a discussion of the difference. sb.append("static "); } + + if (sealedness == Sealedness.FINAL) { + if (typeOfType == UnsolvedClassOrInterfaceType.INTERFACE) { + throw new RuntimeException("Cannot create a final interface."); + } + sb.append("final "); + } else if (sealedness == Sealedness.NON_SEALED) { + sb.append("non-sealed "); + } else if (sealedness == Sealedness.SEALED) { + // Not supported yet because we would need to figure out which classes extend this type + // and add those to a permits clause, and we would also need to apply some sealedness + // to those classes + throw new UnsupportedOperationException( + "Specimin does not support synthetic sealed classes right now."); + } + if (typeOfType == UnsolvedClassOrInterfaceType.INTERFACE) { sb.append("interface "); } else if (typeOfType == UnsolvedClassOrInterfaceType.ANNOTATION) { diff --git a/src/main/java/org/checkerframework/specimin/unsolved/UnsolvedClassOrInterfaceAlternates.java b/src/main/java/org/checkerframework/specimin/unsolved/UnsolvedClassOrInterfaceAlternates.java index 57e0228e..ae157e60 100644 --- a/src/main/java/org/checkerframework/specimin/unsolved/UnsolvedClassOrInterfaceAlternates.java +++ b/src/main/java/org/checkerframework/specimin/unsolved/UnsolvedClassOrInterfaceAlternates.java @@ -1,6 +1,7 @@ package org.checkerframework.specimin.unsolved; import java.util.ArrayList; +import java.util.HashSet; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; @@ -49,6 +50,14 @@ private enum SuperTypeRelationship { */ private boolean alreadyHandledAllSuperRelationships = false; + /** + * A flag to block the addition of super classes after a super class MUST be the only super class. + */ + private boolean superClassAlreadyLocked = false; + + /** A set to know what sealedness this type CANNOT be. */ + private final Set forbiddenSealednesses = new HashSet<>(); + /** A map of super types to their relationships to the type represented by this object. */ private final Map, SuperTypeRelationship> superTypeRelationships = new LinkedHashMap<>(); @@ -184,6 +193,33 @@ protected void addAlternate(UnsolvedClassOrInterface alternate) { this.fullyQualifiedNames.add(alternate.getFullyQualifiedName()); } + /** + * Ensures that the given super class is and will be the only super class for this class. + * + * @param superClass The super class to ensure + */ + public void ensureSuperClass(MemberType superClass) { + if (superClassAlreadyLocked) { + return; + } + + Set> toRemove = new HashSet<>(); + + for (Map.Entry, SuperTypeRelationship> entry : + superTypeRelationships.entrySet()) { + if (entry.getValue() == SuperTypeRelationship.EXTENDS) { + toRemove.add(entry.getKey()); + } + } + + for (Set removeSet : toRemove) { + superTypeRelationships.remove(removeSet); + } + + forceSuperClass(superClass); + superClassAlreadyLocked = true; + } + /** * Adds a set of mutually exclusive super type to this class, with an unknown relationship * (superclass/superinterface). @@ -219,10 +255,14 @@ public void addSuperType(Set superTypes) { Set sanitizedSuperTypes = new LinkedHashSet<>(); for (MemberType superType : superTypes) { - if (superType instanceof UnsolvedMemberType unsolvedMemberType - && unsolvedMemberType.getUnsolvedType().equals(this)) { - // If the super type is this type, we don't need to add it - continue; + if (superType instanceof UnsolvedMemberType unsolvedMemberType) { + // If we're extending a type, that type cannot be final + unsolvedMemberType.getUnsolvedType().removeAndBlockSealedness(Sealedness.FINAL); + + if (unsolvedMemberType.getUnsolvedType().equals(this)) { + // If the super type is this type, we don't need to add it + continue; + } } if (superType.equals(SolvedMemberType.JAVA_LANG_OBJECT)) { // If the super type is java.lang.Object, we don't need to add it @@ -236,9 +276,9 @@ public void addSuperType(Set superTypes) { return; } - if (commonType == UnsolvedClassOrInterfaceType.CLASS) { + if (commonType == UnsolvedClassOrInterfaceType.CLASS && !superClassAlreadyLocked) { superTypeRelationships.put(sanitizedSuperTypes, SuperTypeRelationship.EXTENDS); - } else if (commonType == UnsolvedClassOrInterfaceType.INTERFACE) { + } else if (commonType == UnsolvedClassOrInterfaceType.INTERFACE || superClassAlreadyLocked) { superTypeRelationships.put(sanitizedSuperTypes, SuperTypeRelationship.IMPLEMENTS); } else if (superTypeRelationships.get(superTypes) == null) { superTypeRelationships.put(sanitizedSuperTypes, SuperTypeRelationship.UNKNOWN); @@ -251,6 +291,10 @@ public void addSuperType(Set superTypes) { * @param superClass The super class to force */ public void forceSuperClass(MemberType superClass) { + if (superClassAlreadyLocked) { + return; + } + if ((superClass instanceof UnsolvedMemberType unsolved && unsolved.getUnsolvedType().equals(this)) || superClass.equals(SolvedMemberType.JAVA_LANG_OBJECT)) { @@ -265,6 +309,10 @@ public void forceSuperClass(MemberType superClass) { setType(UnsolvedClassOrInterfaceType.CLASS); } + if (superClass instanceof UnsolvedMemberType unsolvedMemberType) { + unsolvedMemberType.getUnsolvedType().removeAndBlockSealedness(Sealedness.FINAL); + } + superTypeRelationships.put( new LinkedHashSet<>(removeAllWildcardsAndReturnPotentialTypes(superClass)), SuperTypeRelationship.EXTENDS); @@ -292,6 +340,10 @@ public void forceSuperInterface(MemberType superInterface) { return; } + if (superInterface instanceof UnsolvedMemberType unsolvedMemberType) { + unsolvedMemberType.getUnsolvedType().removeAndBlockSealedness(Sealedness.FINAL); + } + if (superInterface.toString().equals("java.lang.annotation.Annotation")) { superTypeRelationships.clear(); setType(UnsolvedClassOrInterfaceType.ANNOTATION); @@ -303,6 +355,74 @@ public void forceSuperInterface(MemberType superInterface) { SuperTypeRelationship.IMPLEMENTS); } + /** + * Adds a sealedness to all alternates. If any alternate already has a sealedness, then new + * alternates are generated. + * + * @param sealedness The sealedness + */ + public void addSealedness(Sealedness sealedness) { + if (forbiddenSealednesses.contains(sealedness)) { + return; + } + + boolean allUnsealedness = + doAllAlternatesReturnTrueFor((alt) -> alt.getSealedness() == Sealedness.NONE); + + if (allUnsealedness) { + for (UnsolvedClassOrInterface alternate : getAlternates()) { + if (sealedness == Sealedness.FINAL + && alternate.getType() != UnsolvedClassOrInterfaceType.CLASS + && alternate.getType() != UnsolvedClassOrInterfaceType.UNKNOWN) { + continue; + } + + alternate.setSealedness(sealedness); + } + } else { + List alternates = new ArrayList<>(); + for (UnsolvedClassOrInterface alternate : getAlternates()) { + if (sealedness == Sealedness.FINAL + && alternate.getType() != UnsolvedClassOrInterfaceType.CLASS + && alternate.getType() != UnsolvedClassOrInterfaceType.UNKNOWN) { + continue; + } + + if (alternate.getSealedness() == sealedness) { + alternates.clear(); + break; + } + + UnsolvedClassOrInterface copy = alternate.copy(); + copy.setSealedness(sealedness); + + alternates.add(copy); + } + + for (UnsolvedClassOrInterface alternate : alternates) { + addAlternate(alternate); + } + } + } + + /** + * Removes alternates with a certain sealedness, and blocks it from being added back in the + * future. + * + * @param sealedness The sealedness + */ + public void removeAndBlockSealedness(Sealedness sealedness) { + forbiddenSealednesses.add(sealedness); + boolean allMatchSealedness = + doAllAlternatesReturnTrueFor((alt) -> alt.getSealedness() == sealedness); + + if (allMatchSealedness) { + applyToAllAlternates(a -> a.setSealedness(Sealedness.NONE)); + } else { + getAlternates().removeIf(a -> a.getSealedness() == sealedness); + } + } + /** * Removes all wildcards from the given member type and generates alternates based on which type * variable could take its place. The best guess for the right type variable is returned as the diff --git a/src/main/java/org/checkerframework/specimin/unsolved/UnsolvedSymbolGenerator.java b/src/main/java/org/checkerframework/specimin/unsolved/UnsolvedSymbolGenerator.java index 68583f81..d7802c47 100644 --- a/src/main/java/org/checkerframework/specimin/unsolved/UnsolvedSymbolGenerator.java +++ b/src/main/java/org/checkerframework/specimin/unsolved/UnsolvedSymbolGenerator.java @@ -2323,6 +2323,7 @@ public UnsolvedGenerationResult addInformation(Node node) { if (syntheticType != null) { syntheticType.setType(UnsolvedClassOrInterfaceType.INTERFACE); + syntheticType.removeAndBlockSealedness(Sealedness.FINAL); } } for (ClassOrInterfaceType extended : decl.getExtendedTypes()) { @@ -2335,6 +2336,27 @@ public UnsolvedGenerationResult addInformation(Node node) { decl.isInterface() ? UnsolvedClassOrInterfaceType.INTERFACE : UnsolvedClassOrInterfaceType.CLASS); + syntheticType.removeAndBlockSealedness(Sealedness.FINAL); + } + } + for (ClassOrInterfaceType permitted : decl.getPermittedTypes()) { + UnsolvedClassOrInterfaceAlternates syntheticType = + (UnsolvedClassOrInterfaceAlternates) + findExistingAndUpdateFQNs(fullyQualifiedNameGenerator.getFQNsFromType(permitted)); + + if (syntheticType != null) { + if (!decl.isInterface()) { + syntheticType.setType(UnsolvedClassOrInterfaceType.CLASS); + syntheticType.ensureSuperClass( + new SolvedMemberType(decl.getFullyQualifiedName().get())); + } else { + syntheticType.forceSuperInterface( + new SolvedMemberType(decl.getFullyQualifiedName().get())); + } + + // Sealedness best effort should be final unless we have evidence against it + syntheticType.addSealedness(Sealedness.FINAL); + syntheticType.addSealedness(Sealedness.NON_SEALED); } } } else if (node instanceof EnumDeclaration decl) { @@ -2345,6 +2367,7 @@ public UnsolvedGenerationResult addInformation(Node node) { if (syntheticType != null) { syntheticType.setType(UnsolvedClassOrInterfaceType.INTERFACE); + syntheticType.removeAndBlockSealedness(Sealedness.FINAL); } } } else if (node instanceof MethodDeclaration methodDecl) { diff --git a/src/test/java/org/checkerframework/specimin/PermitsTest.java b/src/test/java/org/checkerframework/specimin/PermitsTest.java new file mode 100644 index 00000000..ee065fa6 --- /dev/null +++ b/src/test/java/org/checkerframework/specimin/PermitsTest.java @@ -0,0 +1,16 @@ +package org.checkerframework.specimin; + +import java.io.IOException; +import org.junit.jupiter.api.Test; + +/** + * This test checks that types in permits clauses are preserved, in addition to relevant keywords + * like final, sealed, and non-sealed in child classes. + */ +public class PermitsTest { + @Test + public void runTest() throws IOException { + SpeciminTestExecutor.runTestWithoutJarPaths( + "permits", new String[] {"com/example/Foo.java"}, new String[] {"com.example.Foo#foo()"}); + } +} diff --git a/src/test/java/org/checkerframework/specimin/UnsolvedPermitsTest.java b/src/test/java/org/checkerframework/specimin/UnsolvedPermitsTest.java new file mode 100644 index 00000000..98d37d1a --- /dev/null +++ b/src/test/java/org/checkerframework/specimin/UnsolvedPermitsTest.java @@ -0,0 +1,18 @@ +package org.checkerframework.specimin; + +import java.io.IOException; +import org.junit.jupiter.api.Test; + +/** + * This test checks that unsolved types in permits clauses are generated with a correct keyword (in + * best effort run mode, this would be final, unless we have evidence otherwise). + */ +public class UnsolvedPermitsTest { + @Test + public void runTest() throws IOException { + SpeciminTestExecutor.runTestWithoutJarPaths( + "unsolvedpermits", + new String[] {"com/example/Foo.java"}, + new String[] {"com.example.Foo#foo()"}); + } +} diff --git a/src/test/resources/permits/expected/com/example/Foo.java b/src/test/resources/permits/expected/com/example/Foo.java new file mode 100644 index 00000000..85143ddb --- /dev/null +++ b/src/test/resources/permits/expected/com/example/Foo.java @@ -0,0 +1,11 @@ +package com.example; + +public sealed class Foo permits Bar, Baz, Qux { + void foo() { } +} + +final class Bar extends Foo { } +non-sealed class Baz extends Foo { } +sealed class Qux extends Foo permits Quux { } + +final class Quux extends Qux { } \ No newline at end of file diff --git a/src/test/resources/permits/input/com/example/Foo.java b/src/test/resources/permits/input/com/example/Foo.java new file mode 100644 index 00000000..1115394c --- /dev/null +++ b/src/test/resources/permits/input/com/example/Foo.java @@ -0,0 +1,13 @@ +package com.example; + +public sealed class Foo permits Bar, Baz, Qux { + void foo() { + + } +} + +final class Bar extends Foo { } +non-sealed class Baz extends Foo { } +sealed class Qux extends Foo permits Quux { } + +final class Quux extends Qux { } \ No newline at end of file diff --git a/src/test/resources/unsolvedpermits/expected/com/example/Bar.java b/src/test/resources/unsolvedpermits/expected/com/example/Bar.java new file mode 100644 index 00000000..7b51d9ec --- /dev/null +++ b/src/test/resources/unsolvedpermits/expected/com/example/Bar.java @@ -0,0 +1,3 @@ +package com.example; + +public final class Bar extends com.example.Foo { } \ No newline at end of file diff --git a/src/test/resources/unsolvedpermits/expected/com/example/Baz.java b/src/test/resources/unsolvedpermits/expected/com/example/Baz.java new file mode 100644 index 00000000..b078e00f --- /dev/null +++ b/src/test/resources/unsolvedpermits/expected/com/example/Baz.java @@ -0,0 +1,3 @@ +package com.example; + +public non-sealed class Baz extends com.example.Foo { } \ No newline at end of file diff --git a/src/test/resources/unsolvedpermits/expected/com/example/Foo.java b/src/test/resources/unsolvedpermits/expected/com/example/Foo.java new file mode 100644 index 00000000..8f1ad64e --- /dev/null +++ b/src/test/resources/unsolvedpermits/expected/com/example/Foo.java @@ -0,0 +1,9 @@ +package com.example; + +public sealed class Foo permits Bar, Baz { + void foo() { + Qux qux; + } +} + +class Qux extends Baz { } \ No newline at end of file diff --git a/src/test/resources/unsolvedpermits/input/com/example/Foo.java b/src/test/resources/unsolvedpermits/input/com/example/Foo.java new file mode 100644 index 00000000..7de8ccf3 --- /dev/null +++ b/src/test/resources/unsolvedpermits/input/com/example/Foo.java @@ -0,0 +1,10 @@ +package com.example; + +public sealed class Foo permits Bar, Baz { + void foo() { + Qux qux; + } +} + +// Since Qux extends Baz, Baz cannot be final +class Qux extends Baz { } \ No newline at end of file