Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,9 @@ public List<Node> 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<MethodDeclaration> mustImplement =
JavaParserUtil.getDeclarationsForAllMustImplementMethods(
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
*
* <p>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
}
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down Expand Up @@ -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.
*
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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.");
}

Comment thread
theron-wang marked this conversation as resolved.
if (typeOfType == UnsolvedClassOrInterfaceType.INTERFACE) {
sb.append("interface ");
} else if (typeOfType == UnsolvedClassOrInterfaceType.ANNOTATION) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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<Sealedness> forbiddenSealednesses = new HashSet<>();

/** A map of super types to their relationships to the type represented by this object. */
private final Map<Set<MemberType>, SuperTypeRelationship> superTypeRelationships =
new LinkedHashMap<>();
Expand Down Expand Up @@ -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<Set<MemberType>> toRemove = new HashSet<>();

for (Map.Entry<Set<MemberType>, SuperTypeRelationship> entry :
superTypeRelationships.entrySet()) {
if (entry.getValue() == SuperTypeRelationship.EXTENDS) {
toRemove.add(entry.getKey());
}
}

for (Set<MemberType> removeSet : toRemove) {
superTypeRelationships.remove(removeSet);
}

forceSuperClass(superClass);
superClassAlreadyLocked = true;
}
Comment thread
theron-wang marked this conversation as resolved.

/**
* Adds a set of mutually exclusive super type to this class, with an unknown relationship
* (superclass/superinterface).
Expand Down Expand Up @@ -219,10 +255,14 @@ public void addSuperType(Set<MemberType> superTypes) {

Set<MemberType> 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
Expand All @@ -236,9 +276,9 @@ public void addSuperType(Set<MemberType> 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);
Expand All @@ -251,6 +291,10 @@ public void addSuperType(Set<MemberType> 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)) {
Expand All @@ -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);
Expand Down Expand Up @@ -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);
Expand All @@ -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<UnsolvedClassOrInterface> alternates = new ArrayList<>();
for (UnsolvedClassOrInterface alternate : getAlternates()) {
if (sealedness == Sealedness.FINAL
&& alternate.getType() != UnsolvedClassOrInterfaceType.CLASS
&& alternate.getType() != UnsolvedClassOrInterfaceType.UNKNOWN) {
continue;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()) {
Expand All @@ -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);
}
}
Comment thread
theron-wang marked this conversation as resolved.
} else if (node instanceof EnumDeclaration decl) {
Expand All @@ -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) {
Expand Down
16 changes: 16 additions & 0 deletions src/test/java/org/checkerframework/specimin/PermitsTest.java
Original file line number Diff line number Diff line change
@@ -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()"});
}
}
Original file line number Diff line number Diff line change
@@ -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()"});
}
}
11 changes: 11 additions & 0 deletions src/test/resources/permits/expected/com/example/Foo.java
Original file line number Diff line number Diff line change
@@ -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 { }
Loading
Loading