diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index 611d3af64..f2a8f7bb0 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -29,7 +29,10 @@ jobs: - name: Setup Gradle uses: gradle/gradle-build-action@v2 - name: Run build with Gradle Wrapper - run: ./gradlew build expectedTestOutputsMustCompile + continue-on-error: true + run: | + ./gradlew build expectedTestOutputsMustCompile + echo "exit_code_expectedTestOutputsMustCompile=$?" >> $GITHUB_OUTPUT - name: export Specimin PATH run: echo "SPECIMIN=$(pwd)" >> $GITHUB_ENV - name: Setup Python @@ -60,6 +63,12 @@ jobs: diff -uw src/main/resources/min_program_compile_status.json specimin-evaluation/ISSUES/compile_status.json - name: Check preservation status run: diff -uw src/main/resources/preservation_status.json specimin-evaluation/ISSUES/preservation_status.json + - name: Fail if expectedTestOutputsMustCompile failed + run: | + if [[ "${{ steps.build.outputs.exit_code_expectedTestOutputsMustCompile }}" != "0" ]]; then + echo "expectedTestOutputsMustCompile failed." + exit 1 + fi windows-tester: runs-on: windows-latest steps: diff --git a/build.gradle b/build.gradle index 538b18bb1..4291d5166 100644 --- a/build.gradle +++ b/build.gradle @@ -4,9 +4,9 @@ plugins { id("java-library") id("maven-publish") id("signing") - id("com.diffplug.spotless").version("6.25.0") - id("org.checkerframework").version("0.6.49") - id("net.ltgt.errorprone").version("4.1.0") + id("com.diffplug.spotless").version("7.1.0") + id("org.checkerframework").version("0.6.56") + id("net.ltgt.errorprone").version("4.3.0") id("com.adarshr.test-logger").version("4.0.0") } @@ -22,18 +22,20 @@ application { dependencies { - implementation("com.github.javaparser:javaparser-symbol-solver-core:3.26.1") + implementation("com.github.javaparser:javaparser-symbol-solver-core:3.27.0") implementation("net.sf.jopt-simple:jopt-simple:5.0.4") - implementation("org.vineflower:vineflower:1.11.0") + implementation("org.vineflower:vineflower:1.11.1") - implementation("commons-io:commons-io:2.18.0") + implementation("commons-io:commons-io:2.19.0") // Use JUnit test framework. testImplementation("junit:junit:4.13.2") - errorprone("com.google.errorprone:error_prone_core:2.35.1") + errorprone("com.google.errorprone:error_prone_core:2.40.0") + + implementation 'com.google.googlejavaformat:google-java-format:1.28.0' } // Use require-javadoc. From https://github.com/plume-lib/require-javadoc. @@ -41,7 +43,7 @@ configurations { requireJavadoc } dependencies { - requireJavadoc("org.plumelib:require-javadoc:1.0.9") + requireJavadoc("org.plumelib:require-javadoc:2.0.0") } tasks.register("requireJavadoc", JavaExec) { group = "Documentation" @@ -73,6 +75,29 @@ tasks.register("checkExpectedOutputCompilesFor", Exec) { } } +// needed for google-java-format: see https://github.com/google/google-java-format?tab=readme-ov-file#as-a-library +tasks.withType(JavaExec).configureEach { + jvmArgs += [ + '--add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED', + '--add-exports=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED', + '--add-exports=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED', + '--add-exports=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED', + '--add-exports=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED', + '--add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED' + ] +} + +tasks.withType(Test).configureEach { + jvmArgs += [ + '--add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED', + '--add-exports=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED', + '--add-exports=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED', + '--add-exports=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED', + '--add-exports=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED', + '--add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED' + ] +} + tasks.compileJava { // uncomment for testing // options.errorprone.enabled = false @@ -113,7 +138,7 @@ spotless { // define the steps to apply to those files trimTrailingWhitespace() - indentWithSpaces() + leadingTabsToSpaces() endWithNewline() } java { diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index c1962a79e..e6441136f 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 20db9ad5c..2a84e188b 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.0.0-bin.zip networkTimeout=10000 +validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index aeb74cbb4..1aa94a426 100755 --- a/gradlew +++ b/gradlew @@ -83,7 +83,8 @@ done # This is normally unused # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} -APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum @@ -130,10 +131,13 @@ location of your Java installation." fi else JAVACMD=java - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." + fi fi # Increase the maximum file descriptors if we can. @@ -141,7 +145,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 + # shellcheck disable=SC2039,SC3045 MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac @@ -149,7 +153,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then '' | soft) :;; #( *) # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 + # shellcheck disable=SC2039,SC3045 ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac @@ -198,11 +202,11 @@ fi # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' -# Collect all arguments for the java command; -# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of -# shell script including quotes and variable substitutions, so put them in -# double quotes to make sure that they get re-expanded; and -# * put everything else in single quotes, so that it's not re-expanded. +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ diff --git a/gradlew.bat b/gradlew.bat index 93e3f59f1..25da30dbd 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -43,11 +43,11 @@ set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if %ERRORLEVEL% equ 0 goto execute -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail @@ -57,11 +57,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail diff --git a/settings.gradle b/settings.gradle index 8c83fc040..40a0d503f 100644 --- a/settings.gradle +++ b/settings.gradle @@ -8,4 +8,3 @@ */ rootProject.name = "specimin" -include("lib") diff --git a/src/main/java/org/checkerframework/specimin/AmbiguityResolutionPolicy.java b/src/main/java/org/checkerframework/specimin/AmbiguityResolutionPolicy.java new file mode 100644 index 000000000..1cd53a997 --- /dev/null +++ b/src/main/java/org/checkerframework/specimin/AmbiguityResolutionPolicy.java @@ -0,0 +1,31 @@ +package org.checkerframework.specimin; + +import com.google.common.base.Ascii; + +/** Represents the different ambiguity resolution policies. */ +public enum AmbiguityResolutionPolicy { + /** Generates all alternates */ + All, + /** Generates the best effort alternates */ + BestEffort, + /** Outputs the best set of alternates based on an input condition */ + InputCondition; + + /** + * Gets the corresponding ambiguity resolution policy based on the input. Throws if invalid. + * + * @param input The input; accepts all, best-effort, and input-condition. + * @return The enum value, if valid, or else an exception + */ + public static AmbiguityResolutionPolicy parse(String input) { + return switch (Ascii.toLowerCase(input)) { + case "all" -> AmbiguityResolutionPolicy.All; + case "best-effort" -> AmbiguityResolutionPolicy.BestEffort; + case "input-condition" -> AmbiguityResolutionPolicy.InputCondition; + default -> + throw new RuntimeException( + "Unsupported ambiguity resolution policy. Options are: \"all\", \"best-effort\"," + + " \"input-condition\""); + }; + } +} diff --git a/src/main/java/org/checkerframework/specimin/AnnotationParameterTypesVisitor.java b/src/main/java/org/checkerframework/specimin/AnnotationParameterTypesVisitor.java deleted file mode 100644 index 8b00f2064..000000000 --- a/src/main/java/org/checkerframework/specimin/AnnotationParameterTypesVisitor.java +++ /dev/null @@ -1,288 +0,0 @@ -package org.checkerframework.specimin; - -import com.github.javaparser.ast.ImportDeclaration; -import com.github.javaparser.ast.Node; -import com.github.javaparser.ast.NodeList; -import com.github.javaparser.ast.PackageDeclaration; -import com.github.javaparser.ast.body.AnnotationMemberDeclaration; -import com.github.javaparser.ast.expr.AnnotationExpr; -import com.github.javaparser.ast.expr.ArrayInitializerExpr; -import com.github.javaparser.ast.expr.Expression; -import com.github.javaparser.ast.expr.MarkerAnnotationExpr; -import com.github.javaparser.ast.expr.MemberValuePair; -import com.github.javaparser.ast.expr.NormalAnnotationExpr; -import com.github.javaparser.ast.expr.SingleMemberAnnotationExpr; -import com.github.javaparser.ast.visitor.Visitable; -import com.github.javaparser.resolution.UnsolvedSymbolException; -import com.github.javaparser.resolution.types.ResolvedType; -import com.github.javaparser.symbolsolver.reflectionmodel.ReflectionAnnotationDeclaration; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Optional; -import java.util.Set; - -/** - * Preserve annotations and their parameter types for used classes. This will only keep annotations - * if the corresponding class, method, or field declaration is marked to be preserved. If an - * annotation (or its parameters) is not resolvable, it will be removed. - */ -public class AnnotationParameterTypesVisitor extends SpeciminStateVisitor { - /** - * Constructs a new AnnotationParameterTypesVisitor with the previous visitor - * - * @param previousVisitor the last visitor to run before this one - */ - public AnnotationParameterTypesVisitor(SpeciminStateVisitor previousVisitor) { - super(previousVisitor); - } - - /** Set containing the signatures of classes used by annotations. */ - private Set classesToAdd = new HashSet<>(); - - /** Map containing the signatures of static imports. */ - Map staticImports = new HashMap<>(); - - /** - * Get the set containing the signatures of classes used by annotations. - * - * @return The set containing the signatures of classes used by annotations. - */ - public Set getClassesToAdd() { - return classesToAdd; - } - - @Override - public Node visit(ImportDeclaration decl, Void p) { - if (decl.isStatic()) { - String fullName = decl.getNameAsString(); - String memberName = fullName.substring(fullName.lastIndexOf(".") + 1); - staticImports.put(memberName, fullName); - } - return decl; - } - - @Override - public Visitable visit(AnnotationMemberDeclaration decl, Void p) { - // Ensure that enums/fields that are used by default are included - // Also, preserve method type since a definition with a default value may be - // added, but that value type is never explored by the visit(AnnotationExpr) methods - // For example, when the type is an enum (Foo), the definition may set a default value to - // Foo.VALUE, but Foo.VALUE may never be referenced in an @Annotation() usage (instead, - // other Foo values may) be used, so Foo.VALUE would be removed by PrunerVisitor and result - // in compile errors. - if (usedTypeElements.contains(JavaParserUtil.getEnclosingClassName(decl))) { - // Class<> from jar files may contain other classes - if (decl.getType().toString().startsWith("Class<")) { - // Replace with Class to prevent compile-time errors - String type = "Class"; - if (decl.getType().isArrayType()) { - type += "[]"; - } - decl.setType(type); - } else { - try { - ResolvedType resolved = decl.getType().resolve(); - if (resolved.isArray()) { - resolved = resolved.asArrayType().getComponentType(); - } - if (resolved.isReferenceType()) { - usedTypeElements.add(resolved.asReferenceType().getQualifiedName()); - } - } catch (UnsolvedSymbolException ex) { - // TODO: retrigger synthetic type generation - return super.visit(decl, p); - } - } - Optional defaultValue = decl.getDefaultValue(); - if (defaultValue.isPresent()) { - Set usedClassByCurrentAnnotation = new HashSet<>(); - Set usedMembersByCurrentAnnotation = new HashSet<>(); - boolean resolvable = - handleAnnotationValue( - defaultValue.get(), usedClassByCurrentAnnotation, usedMembersByCurrentAnnotation); - - if (resolvable) { - classesToAdd.addAll(usedClassByCurrentAnnotation); - usedMembers.addAll(usedMembersByCurrentAnnotation); - } - } - } - return super.visit(decl, p); - } - - @Override - public Visitable visit(MarkerAnnotationExpr anno, Void p) { - // Annotations on packages cause an exception in findClosestParentMemberOrClassLike - if (anno.hasParentNode() && anno.getParentNode().get() instanceof PackageDeclaration) { - return super.visit(anno, p); - } - - Node parent = JavaParserUtil.findClosestParentMemberOrClassLike(anno); - - if (isTargetOrUsed(parent)) { - handleAnnotation(anno); - } - return super.visit(anno, p); - } - - @Override - public Visitable visit(SingleMemberAnnotationExpr anno, Void p) { - // Annotations on packages cause an exception in findClosestParentMemberOrClassLike - if (anno.hasParentNode() && anno.getParentNode().get() instanceof PackageDeclaration) { - return super.visit(anno, p); - } - - Node parent = JavaParserUtil.findClosestParentMemberOrClassLike(anno); - - if (isTargetOrUsed(parent)) { - handleAnnotation(anno); - } - return super.visit(anno, p); - } - - @Override - public Visitable visit(NormalAnnotationExpr anno, Void p) { - // Annotations on packages cause an exception in findClosestParentMemberOrClassLike - if (anno.hasParentNode() && anno.getParentNode().get() instanceof PackageDeclaration) { - return super.visit(anno, p); - } - - Node parent = JavaParserUtil.findClosestParentMemberOrClassLike(anno); - - if (isTargetOrUsed(parent)) { - handleAnnotation(anno); - } - return super.visit(anno, p); - } - - /** - * Helper method to add an annotation to the usedClass set, including the types used in annotation - * parameters. - * - * @param anno The annotation to process - */ - private void handleAnnotation(AnnotationExpr anno) { - Set usedClassByCurrentAnnotation = new HashSet<>(); - Set usedMembersByCurrentAnnotation = new HashSet<>(); - boolean resolvable = true; - try { - String qualifiedName = anno.resolve().getQualifiedName(); - if (anno.resolve() instanceof ReflectionAnnotationDeclaration - && !JavaLangUtils.inJdkPackage(qualifiedName)) { - // This usually means that JavaParser has resolved this through the import, but there - // is no file/CompilationUnit behind it, so we should discard it to prevent compile errors - anno.remove(); - return; - } - } catch (UnsolvedSymbolException ex) { - anno.remove(); - return; - } - - if (anno.isSingleMemberAnnotationExpr()) { - Expression value = anno.asSingleMemberAnnotationExpr().getMemberValue(); - resolvable = - handleAnnotationValue( - value, usedClassByCurrentAnnotation, usedMembersByCurrentAnnotation); - } else if (anno.isNormalAnnotationExpr()) { - for (MemberValuePair pair : anno.asNormalAnnotationExpr().getPairs()) { - Expression value = pair.getValue(); - resolvable = - handleAnnotationValue( - value, usedClassByCurrentAnnotation, usedMembersByCurrentAnnotation); - if (!resolvable) { - break; - } - } - } - - // Only add annotation to the usedClass set if all parameters are resolvable - if (resolvable) { - usedClassByCurrentAnnotation.add(anno.resolve().getQualifiedName()); - classesToAdd.addAll(usedClassByCurrentAnnotation); - usedMembers.addAll(usedMembersByCurrentAnnotation); - } else { - // Remove unsolvable annotations; these parameter types are unsolvable since - // the UnsolvedSymbolVisitor did not create synthetic types for annotations - // included later on - anno.remove(); - } - } - - /** - * Handles annotation parameter value types, adding all used types to the usedByCurrentAnnotation - * set. This method can handle array types as well as annotations referenced in the parameters. If - * the type is a primitive or String, there is no effect. - * - * @return true if value is resolvable, false if not - */ - private boolean handleAnnotationValue( - Expression value, - Set usedClassByCurrentAnnotation, - Set usedMembersByCurrentAnnotation) { - if (value.isArrayInitializerExpr()) { - ArrayInitializerExpr array = value.asArrayInitializerExpr(); - NodeList values = array.getValues(); - - if (values.isEmpty()) { - return true; - } - for (Expression val : values) { - handleAnnotationValue(val, usedClassByCurrentAnnotation, usedMembersByCurrentAnnotation); - } - return true; - } else if (value.isClassExpr()) { - try { - ResolvedType resolved = value.asClassExpr().getType().resolve(); - - if (resolved.isReferenceType()) { - usedClassByCurrentAnnotation.add(resolved.asReferenceType().getQualifiedName()); - } - } catch (UnsolvedSymbolException ex) { - // TODO: retrigger synthetic type generation - return false; - } - return true; - } else if (value.isFieldAccessExpr()) { - try { - ResolvedType resolved = value.asFieldAccessExpr().calculateResolvedType(); - - if (resolved.isReferenceType()) { - String parentName = resolved.asReferenceType().getQualifiedName(); - usedClassByCurrentAnnotation.add(parentName); - String memberName = value.asFieldAccessExpr().getNameAsString(); - // member here could be an enum or a field - usedMembersByCurrentAnnotation.add(parentName + "#" + memberName); - usedMembersByCurrentAnnotation.add(parentName + "." + memberName); - } - } catch (UnsolvedSymbolException ex) { - // TODO: retrigger synthetic type generation - return false; - } - return true; - } else if (value.isNameExpr()) { // variable of some sort - try { - ResolvedType resolved = value.asNameExpr().calculateResolvedType(); - - if (resolved.isReferenceType()) { - usedClassByCurrentAnnotation.add(resolved.asReferenceType().getQualifiedName()); - String fullStaticName = staticImports.get(value.asNameExpr().getNameAsString()); - if (fullStaticName != null) { - String parentName = fullStaticName.substring(0, fullStaticName.lastIndexOf(".")); - String memberName = fullStaticName.substring(fullStaticName.lastIndexOf(".") + 1); - // static import here could be an enum or a field - usedClassByCurrentAnnotation.add(parentName); - usedMembersByCurrentAnnotation.add(parentName + "#" + memberName); - usedMembersByCurrentAnnotation.add(fullStaticName); - } - } - } catch (UnsolvedSymbolException ex) { - // TODO: retrigger synthetic type generation - return false; - } - return true; - } - return true; - } -} diff --git a/src/main/java/org/checkerframework/specimin/AnnotationTargetRemoverVisitor.java b/src/main/java/org/checkerframework/specimin/AnnotationTargetRemoverVisitor.java deleted file mode 100644 index c0fd4b775..000000000 --- a/src/main/java/org/checkerframework/specimin/AnnotationTargetRemoverVisitor.java +++ /dev/null @@ -1,254 +0,0 @@ -package org.checkerframework.specimin; - -import com.github.javaparser.StaticJavaParser; -import com.github.javaparser.ast.Node; -import com.github.javaparser.ast.PackageDeclaration; -import com.github.javaparser.ast.body.AnnotationDeclaration; -import com.github.javaparser.ast.body.AnnotationMemberDeclaration; -import com.github.javaparser.ast.body.ConstructorDeclaration; -import com.github.javaparser.ast.body.EnumConstantDeclaration; -import com.github.javaparser.ast.body.FieldDeclaration; -import com.github.javaparser.ast.body.MethodDeclaration; -import com.github.javaparser.ast.body.Parameter; -import com.github.javaparser.ast.body.RecordDeclaration; -import com.github.javaparser.ast.body.TypeDeclaration; -import com.github.javaparser.ast.expr.AnnotationExpr; -import com.github.javaparser.ast.expr.ArrayInitializerExpr; -import com.github.javaparser.ast.expr.Expression; -import com.github.javaparser.ast.expr.FieldAccessExpr; -import com.github.javaparser.ast.expr.MarkerAnnotationExpr; -import com.github.javaparser.ast.expr.NameExpr; -import com.github.javaparser.ast.expr.NormalAnnotationExpr; -import com.github.javaparser.ast.expr.SingleMemberAnnotationExpr; -import com.github.javaparser.ast.expr.VariableDeclarationExpr; -import com.github.javaparser.ast.type.TypeParameter; -import com.github.javaparser.ast.visitor.ModifierVisitor; -import com.github.javaparser.ast.visitor.Visitable; -import java.lang.annotation.ElementType; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; - -/** - * Removes all unnecessary ElementType values from each annotation declaration, based on their - * usages. Run this visitor after PrunerVisitor to ensure all unnecessary ElementTypes are removed. - */ -public class AnnotationTargetRemoverVisitor extends ModifierVisitor { - /** A Map of fully qualified annotation names to its ElementTypes. */ - private final Map> annotationToElementTypes = new HashMap<>(); - - /** A Map of fully qualified annotation names to their declarations. */ - private final Map annotationToDeclaration = new HashMap<>(); - - /** Constant for the package of {@code @Target} ({@code java.lang.annotation}) */ - private static final String TARGET_PACKAGE = "java.lang.annotation"; - - /** Constant for {@code @Target}'s name (does not include the {@code @}) */ - private static final String TARGET_NAME = "Target"; - - /** Constant for {@code @Target}'s fully qualified name */ - private static final String FULLY_QUALIFIED_TARGET = TARGET_PACKAGE + "." + TARGET_NAME; - - /** - * Updates all annotation declaration {@code @Target} values to match their usages. Only removes - * {@code ElementType}s, doesn't add them. Call this method once after visiting all files. - */ - public void removeExtraAnnotationTargets() { - for (Map.Entry pair : annotationToDeclaration.entrySet()) { - AnnotationExpr targetAnnotation = - pair.getValue().getAnnotationByName(TARGET_NAME).orElse(null); - - if (targetAnnotation == null) { - // This is most likely an existing definition. If there is no @Target annotation, - // we shouldn't add it - continue; - } - - boolean useFullyQualified = targetAnnotation.getNameAsString().equals(FULLY_QUALIFIED_TARGET); - - // Only handle java.lang.annotation.Target - if (!targetAnnotation.resolve().getQualifiedName().equals(FULLY_QUALIFIED_TARGET)) { - continue; - } - - Set actualElementTypes = annotationToElementTypes.get(pair.getKey()); - - if (actualElementTypes == null) { - // No usages of the annotation itself (see Issue272Test) - actualElementTypes = new HashSet<>(); - } - - Set elementTypes = new HashSet<>(); - Set staticallyImportedElementTypes = new HashSet<>(); - Expression memberValue; - - // @Target(ElementType.___) - // @Target({ElementType.___, ElementType.___}) - if (targetAnnotation.isSingleMemberAnnotationExpr()) { - SingleMemberAnnotationExpr asSingleMember = targetAnnotation.asSingleMemberAnnotationExpr(); - memberValue = asSingleMember.getMemberValue(); - } - // @Target(value = ElementType.___) - // @Target(value = {ElementType.___, ElementType.___}) - else if (targetAnnotation.isNormalAnnotationExpr()) { - NormalAnnotationExpr asNormal = targetAnnotation.asNormalAnnotationExpr(); - memberValue = asNormal.getPairs().get(0).getValue(); - } else { - throw new RuntimeException("@Target annotation must contain an ElementType"); - } - - // If there's only one ElementType, we can't remove anything - // We should only be removing ElementTypes, not adding: we do not want to - // convert a non TYPE_USE annotation to a TYPE_USE annotation, for example - if (memberValue.isFieldAccessExpr() || memberValue.isNameExpr()) { - continue; - } else if (memberValue.isArrayInitializerExpr()) { - ArrayInitializerExpr arrayExpr = memberValue.asArrayInitializerExpr(); - if (arrayExpr.getValues().size() <= 1) { - continue; - } - for (Expression value : arrayExpr.getValues()) { - if (value.isFieldAccessExpr()) { - FieldAccessExpr fieldAccessExpr = value.asFieldAccessExpr(); - ElementType elementType = ElementType.valueOf(fieldAccessExpr.getNameAsString()); - if (actualElementTypes.contains(elementType)) { - elementTypes.add(elementType); - } - } else if (value.isNameExpr()) { - // In case of static imports - NameExpr nameExpr = value.asNameExpr(); - ElementType elementType = ElementType.valueOf(nameExpr.getNameAsString()); - if (actualElementTypes.contains(elementType)) { - elementTypes.add(elementType); - staticallyImportedElementTypes.add(elementType); - } - } - } - } - - StringBuilder newAnnotation = new StringBuilder(); - newAnnotation.append("@"); - - if (useFullyQualified) { - newAnnotation.append(TARGET_PACKAGE); - newAnnotation.append("."); - } - - newAnnotation.append(TARGET_NAME); - newAnnotation.append("("); - - newAnnotation.append('{'); - - List sortedElementTypes = new ArrayList<>(elementTypes); - Collections.sort(sortedElementTypes, (a, b) -> a.name().compareTo(b.name())); - - for (int i = 0; i < sortedElementTypes.size(); i++) { - ElementType elementType = sortedElementTypes.get(i); - if (!staticallyImportedElementTypes.contains(elementType)) { - if (useFullyQualified) { - newAnnotation.append(TARGET_PACKAGE); - newAnnotation.append("."); - } - newAnnotation.append("ElementType."); - } - newAnnotation.append(elementType.name()); - - if (i < sortedElementTypes.size() - 1) { - newAnnotation.append(", "); - } - } - - newAnnotation.append("})"); - AnnotationExpr trimmed = StaticJavaParser.parseAnnotation(newAnnotation.toString()); - - targetAnnotation.remove(); - - pair.getValue().addAnnotation(trimmed); - } - } - - @Override - public Visitable visit(AnnotationDeclaration decl, Void p) { - annotationToDeclaration.put(decl.getFullyQualifiedName().get(), decl); - return super.visit(decl, p); - } - - @Override - public Visitable visit(MarkerAnnotationExpr anno, Void p) { - updateAnnotationElementTypes(anno); - return super.visit(anno, p); - } - - @Override - public Visitable visit(NormalAnnotationExpr anno, Void p) { - updateAnnotationElementTypes(anno); - return super.visit(anno, p); - } - - @Override - public Visitable visit(SingleMemberAnnotationExpr anno, Void p) { - updateAnnotationElementTypes(anno); - return super.visit(anno, p); - } - - /** - * Helper method to update the ElementTypes for an annotation. - * - * @param anno The annotation to update element types for - */ - private void updateAnnotationElementTypes(AnnotationExpr anno) { - Node parent = anno.getParentNode().orElse(null); - - if (parent == null) { - return; - } - - Set elementTypes = annotationToElementTypes.get(anno.resolve().getQualifiedName()); - if (elementTypes == null) { - elementTypes = new HashSet<>(); - annotationToElementTypes.put(anno.resolve().getQualifiedName(), elementTypes); - } - - if (parent instanceof AnnotationDeclaration) { - elementTypes.add(ElementType.ANNOTATION_TYPE); - elementTypes.add(ElementType.TYPE); - } else if (parent instanceof ConstructorDeclaration) { - elementTypes.add(ElementType.CONSTRUCTOR); - } else if (parent instanceof FieldDeclaration || parent instanceof EnumConstantDeclaration) { - elementTypes.add(ElementType.FIELD); - } else if (parent instanceof VariableDeclarationExpr) { - elementTypes.add(ElementType.LOCAL_VARIABLE); - } else if (parent instanceof MethodDeclaration) { - elementTypes.add(ElementType.METHOD); - - if (((MethodDeclaration) parent).getType().isVoidType()) { - // If it's void we don't need to add TYPE_USE - return; - } - } else if (parent instanceof AnnotationMemberDeclaration) { - elementTypes.add(ElementType.METHOD); - } else if (parent instanceof PackageDeclaration) { - elementTypes.add(ElementType.PACKAGE); - return; - } else if (parent instanceof Parameter) { - if (parent.getParentNode().isPresent() - && parent.getParentNode().get() instanceof RecordDeclaration) { - elementTypes.add(ElementType.RECORD_COMPONENT); - } else { - elementTypes.add(ElementType.PARAMETER); - } - } else if (parent instanceof TypeDeclaration) { - // TypeDeclaration is the parent class for class, interface, annotation, record declarations - // https://www.javadoc.io/doc/com.github.javaparser/javaparser-core/latest/com/github/javaparser/ast/body/TypeDeclaration.html - elementTypes.add(ElementType.TYPE); - } else if (parent instanceof TypeParameter) { - elementTypes.add(ElementType.TYPE_PARAMETER); - } - - elementTypes.add(ElementType.TYPE_USE); - } -} diff --git a/src/main/java/org/checkerframework/specimin/EnumVisitor.java b/src/main/java/org/checkerframework/specimin/EnumVisitor.java deleted file mode 100644 index 234f22ac9..000000000 --- a/src/main/java/org/checkerframework/specimin/EnumVisitor.java +++ /dev/null @@ -1,88 +0,0 @@ -package org.checkerframework.specimin; - -import com.github.javaparser.ast.body.MethodDeclaration; -import com.github.javaparser.ast.expr.Expression; -import com.github.javaparser.ast.expr.FieldAccessExpr; -import com.github.javaparser.ast.expr.NameExpr; -import com.github.javaparser.ast.visitor.Visitable; -import com.github.javaparser.resolution.UnsolvedSymbolException; -import com.github.javaparser.resolution.declarations.ResolvedValueDeclaration; -import com.github.javaparser.resolution.types.ResolvedType; - -/** - * This visitor updates the list of used classes based on the enum constants used inside the target - * methods. - */ -public class EnumVisitor extends SpeciminStateVisitor { - - /** - * Constructor matching super. - * - * @param previous the previous Specimin visitor - */ - public EnumVisitor(SpeciminStateVisitor previous) { - super(previous); - } - - @Override - public Visitable visit(MethodDeclaration methodDeclaration, Void arg) { - String methodQualifiedSignature = - this.currentClassQualifiedName - + "#" - + JavaParserUtil.removeMethodReturnTypeSpacesAndAnnotations(methodDeclaration); - if (targetMethods.contains(methodQualifiedSignature)) { - return super.visit(methodDeclaration, arg); - } - return methodDeclaration; - // no need to visit non-target methods. - } - - @Override - public Visitable visit(FieldAccessExpr fieldAccessExpr, Void arg) { - if (insideTargetMember) { - updateUsedEnumForPotentialEnum(fieldAccessExpr); - } - return super.visit(fieldAccessExpr, arg); - } - - @Override - public Visitable visit(NameExpr nameExpr, Void arg) { - if (insideTargetMember) { - updateUsedEnumForPotentialEnum(nameExpr); - } - return super.visit(nameExpr, arg); - } - - /** - * Given an expression that could be an enum, this method updates the list of used enums - * accordingly. - * - * @param expression an expression that could be an enum. - */ - public void updateUsedEnumForPotentialEnum(Expression expression) { - ResolvedValueDeclaration resolvedField; - // JavaParser sometimes consider an enum usage a field access expression, sometimes a name - // expression. - if (expression.isFieldAccessExpr()) { - try { - resolvedField = expression.asFieldAccessExpr().resolve(); - } catch (UnsolvedSymbolException | UnsupportedOperationException e) { - return; - } - } else if (expression.isNameExpr()) { - try { - resolvedField = expression.asNameExpr().resolve(); - } catch (UnsolvedSymbolException | UnsupportedOperationException e) { - return; - } - } else { - throw new RuntimeException( - "Unexpected parameter for updateUsedClassForPotentialEnum: " + expression); - } - - if (resolvedField.isEnumConstant()) { - ResolvedType correspondingEnumDeclaration = resolvedField.asEnumConstant().getType(); - usedTypeElements.add(correspondingEnumDeclaration.describe()); - } - } -} diff --git a/src/main/java/org/checkerframework/specimin/FieldDeclarationsVisitor.java b/src/main/java/org/checkerframework/specimin/FieldDeclarationsVisitor.java deleted file mode 100644 index 100b162e9..000000000 --- a/src/main/java/org/checkerframework/specimin/FieldDeclarationsVisitor.java +++ /dev/null @@ -1,70 +0,0 @@ -package org.checkerframework.specimin; - -import com.github.javaparser.ast.Node; -import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration; -import com.github.javaparser.ast.body.EnumConstantDeclaration; -import com.github.javaparser.ast.body.EnumDeclaration; -import com.github.javaparser.ast.body.FieldDeclaration; -import com.github.javaparser.ast.body.VariableDeclarator; -import com.github.javaparser.ast.expr.ObjectCreationExpr; -import com.github.javaparser.ast.expr.SimpleName; -import com.github.javaparser.ast.visitor.VoidVisitorAdapter; -import java.util.HashMap; -import java.util.Map; -import org.checkerframework.checker.signature.qual.ClassGetSimpleName; - -/** - * This visitor is designed to assist the UnsolvedSymbolVisitor in creating synthetic files for - * unsolved NameExpr instances by listing all the names of declared fields in the current input - * file. It's important to note that this visitor is intended to be used in conjunction with the - * UnsolvedSymbolVisitor, so both visitors will traverse the same Java file. - * Thus, @ClassGetSimpleName names for involved classes should be sufficient. - */ -public class FieldDeclarationsVisitor extends VoidVisitorAdapter { - /** - * A mapping of field names to the @ClassGetSimpleName name of the classes in which they are - * declared. Since inner classes can be involved, a map is used instead of a simple list. - */ - Map fieldNameToClassNameMap; - - /** Constructs a new FieldDeclarationsVisitor. */ - public FieldDeclarationsVisitor() { - fieldNameToClassNameMap = new HashMap<>(); - } - - @Override - public void visit(FieldDeclaration decl, Void p) { - // Fields must be contained in an AST node in Java. - Node parent = decl.getParentNode().orElseThrow(); - - if (parent instanceof ObjectCreationExpr) { - return; - } - SimpleName classNodeSimpleName; - if (parent instanceof ClassOrInterfaceDeclaration) { - ClassOrInterfaceDeclaration classNode = (ClassOrInterfaceDeclaration) parent; - classNodeSimpleName = classNode.getName(); - } else if (parent instanceof EnumDeclaration) { - EnumDeclaration enumNode = (EnumDeclaration) parent; - classNodeSimpleName = enumNode.getName(); - } else if (parent instanceof EnumConstantDeclaration) { - EnumConstantDeclaration enumConstant = (EnumConstantDeclaration) parent; - classNodeSimpleName = enumConstant.getName(); - } else { - throw new RuntimeException("unexpected node type: " + parent.getClass()); - } - String className = classNodeSimpleName.asString(); - for (VariableDeclarator variableDecl : decl.getVariables()) { - fieldNameToClassNameMap.put(variableDecl.getNameAsString(), className); - } - } - - /** - * Get the value of fieldAndItsClass - * - * @return the value of fieldAndItsClass - */ - public Map getFieldAndItsClass() { - return fieldNameToClassNameMap; - } -} diff --git a/src/main/java/org/checkerframework/specimin/GetTypesFullNameVisitor.java b/src/main/java/org/checkerframework/specimin/GetTypesFullNameVisitor.java deleted file mode 100644 index b49d42f03..000000000 --- a/src/main/java/org/checkerframework/specimin/GetTypesFullNameVisitor.java +++ /dev/null @@ -1,80 +0,0 @@ -package org.checkerframework.specimin; - -import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration; -import com.github.javaparser.ast.body.EnumDeclaration; -import com.github.javaparser.ast.type.ClassOrInterfaceType; -import com.github.javaparser.ast.visitor.ModifierVisitor; -import com.github.javaparser.ast.visitor.Visitable; -import com.github.javaparser.resolution.UnsolvedSymbolException; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; - -/** - * A visitor that traverses a Java file's AST and creates a map, associating the file name with the - * set of types used inside it. - */ -public class GetTypesFullNameVisitor extends ModifierVisitor { - - /** The directory path of the Java file. */ - private String fileDirectory = ""; - - /** - * A map that associates the file directory with the set of fully qualified names of types used - * within that file. - */ - private Map> fileAndAssociatedTypes = new HashMap<>(); - - /** - * Get the map of files' directories and types used within those files. - * - * @return the value of fileAndAssociatedTypes - */ - public Map> getFileAndAssociatedTypes() { - return Collections.unmodifiableMap(fileAndAssociatedTypes); - } - - @Override - public Visitable visit(ClassOrInterfaceDeclaration decl, Void p) { - // Nested type and local classes don't have a separate class file. - if (!decl.isNestedType() && !decl.isLocalClassDeclaration()) { - // getFullyQualifiedName is always present for non-local classes. - fileDirectory = decl.getFullyQualifiedName().orElseThrow().replace(".", "/") + ".java"; - fileAndAssociatedTypes.put(fileDirectory, new HashSet<>()); - } - return super.visit(decl, p); - } - - @Override - public Visitable visit(EnumDeclaration decl, Void p) { - // Nested types don't have a separate class file. - if (!decl.isNestedType()) { - // getFullyQualifiedName is always present for non-local classes, but enum declarations can't - // be local before Java 16. We only currently technically only support up to Java 11 - // (due to JavaParser version restrictions). Until we upgrade JavaParser, this is safe. - fileDirectory = decl.getFullyQualifiedName().orElseThrow().replace(".", "/") + ".java"; - fileAndAssociatedTypes.put(fileDirectory, new HashSet<>()); - } - return super.visit(decl, p); - } - - @Override - public Visitable visit(ClassOrInterfaceType type, Void p) { - String typeFullName; - try { - typeFullName = - JavaParserUtil.classOrInterfaceTypeToResolvedReferenceType(type).getQualifiedName(); - } catch (UnsolvedSymbolException | UnsupportedOperationException | IllegalArgumentException e) { - return super.visit(type, p); - } - if (fileAndAssociatedTypes.containsKey(fileDirectory)) { - fileAndAssociatedTypes.get(fileDirectory).add(typeFullName); - return super.visit(type, p); - } else { - throw new RuntimeException( - "Unexpected files and types: " + fileDirectory + ", " + typeFullName); - } - } -} diff --git a/src/main/java/org/checkerframework/specimin/InheritancePreserveVisitor.java b/src/main/java/org/checkerframework/specimin/InheritancePreserveVisitor.java deleted file mode 100644 index 3c351cbef..000000000 --- a/src/main/java/org/checkerframework/specimin/InheritancePreserveVisitor.java +++ /dev/null @@ -1,123 +0,0 @@ -package org.checkerframework.specimin; - -import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration; -import com.github.javaparser.ast.type.ClassOrInterfaceType; -import com.github.javaparser.ast.type.Type; -import com.github.javaparser.ast.type.TypeParameter; -import com.github.javaparser.ast.visitor.ModifierVisitor; -import com.github.javaparser.ast.visitor.Visitable; -import com.github.javaparser.resolution.UnsolvedSymbolException; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Set; - -/** - * This is an auxiliary visitor for TargetMethodFinderVisitor. This InheritancePreserveVisitor makes - * sure that every file belonging to the inheritance chain of a used class is also marked as used. - */ -public class InheritancePreserveVisitor extends ModifierVisitor { - - /** List of classes used by the target methods. */ - public Set usedClass; - - /** List of fully-qualified classnames to be added to the list of used classes. */ - public Set addedClasses = new HashSet<>(); - - /** - * Constructs an InheritancePreserveVisitor with the specified set of used classes. - * - * @param usedClass The set of classes used by the target methods. - */ - public InheritancePreserveVisitor(Set usedClass) { - this.usedClass = usedClass; - } - - /** - * Return the set of classes to be added to the list of used classes. - * - * @return The value of addedClasses. - */ - public Set getAddedClasses() { - Set copyOfAddedClasses = new HashSet<>(); - for (String addedClass : addedClasses) { - // An interface might be added to the list of added class infinitely. Consider this example: - // public interface Baz extends Comparable{} - // If we don't have the condition check below, 'Baz' would be repeatedly added to the list of - // added classes every time it's visited, leading to an infinite loop. - if (usedClass.contains(addedClass)) { - continue; - } - copyOfAddedClasses.add(addedClass); - } - return copyOfAddedClasses; - } - - /** Empty the list of added classes. */ - public void emptyAddedClasses() { - addedClasses = new HashSet<>(); - } - - /** Cheap and dirty trick to avoid an infinite loop TODO: clean this up after the deadline */ - private static HashSet visitedBounds = new HashSet(); - - @Override - public Visitable visit(ClassOrInterfaceDeclaration decl, Void p) { - if (usedClass.contains(decl.resolve().getQualifiedName())) { - if (!decl.getTypeParameters().isEmpty()) { - // preserve the bounds of the type parameters, too - for (TypeParameter tp : decl.getTypeParameters()) { - for (Type bound : tp.getTypeBound()) { - String boundDesc = bound.resolve().describe(); - if (visitedBounds.add(boundDesc)) { - TargetMemberFinderVisitor.updateUsedClassWithQualifiedClassName( - boundDesc, addedClasses, new HashMap<>()); - } - } - } - } - - for (ClassOrInterfaceType extendedType : decl.getExtendedTypes()) { - try { - // Including a non-primary to primary map in this context may lead to an infinite loop, - // especially if the superclass is nested within the current class file, resulting in - // infinite file visits. The TargetMethodFinderVisitor already addresses the updating job - // in such cases. (Refer to the SuperClass test for an example.) - TargetMemberFinderVisitor.updateUsedClassWithQualifiedClassName( - extendedType.resolve().describe(), addedClasses, new HashMap<>()); - if (extendedType.getTypeArguments().isPresent()) { - for (Type typeArgument : extendedType.getTypeArguments().get()) { - TargetMemberFinderVisitor.updateUsedClassWithQualifiedClassName( - typeArgument.resolve().describe(), addedClasses, new HashMap<>()); - } - } - } catch (UnsolvedSymbolException | UnsupportedOperationException e) { - continue; - } - } - - for (ClassOrInterfaceType implementedType : decl.getImplementedTypes()) { - try { - String interfacename = implementedType.resolve().describe(); - if (JavaLangUtils.inJdkPackage(interfacename)) { - // Avoid keeping implementations of java.* classes, because those - // would require us to actually implement them (we can't remove things - // from their definitions). This might technically break our guarantees, but it works - // in practice. TODO: fix this up - continue; - } - TargetMemberFinderVisitor.updateUsedClassWithQualifiedClassName( - interfacename, addedClasses, new HashMap<>()); - if (implementedType.getTypeArguments().isPresent()) { - for (Type typeAgrument : implementedType.getTypeArguments().get()) { - TargetMemberFinderVisitor.updateUsedClassWithQualifiedClassName( - typeAgrument.resolve().describe(), addedClasses, new HashMap<>()); - } - } - } catch (UnsolvedSymbolException | UnsupportedOperationException e) { - continue; - } - } - } - return super.visit(decl, p); - } -} diff --git a/src/main/java/org/checkerframework/specimin/JavaLangUtils.java b/src/main/java/org/checkerframework/specimin/JavaLangUtils.java index b0da6a9ff..960c88f6e 100644 --- a/src/main/java/org/checkerframework/specimin/JavaLangUtils.java +++ b/src/main/java/org/checkerframework/specimin/JavaLangUtils.java @@ -1,5 +1,6 @@ package org.checkerframework.specimin; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Map; @@ -54,6 +55,22 @@ private JavaLangUtils() { /** A map of primitive names (int, short, etc.) to their object representations */ private static final Map primitivesToObjects = new HashMap<>(); + /** + * A map of methods signatures to return types in java.lang.Object. Parameter types are fully + * qualified (if not primitive) and there are spaces after each comma. + */ + private static Map javaLangObjectMethods; + + /** + * A map of method signatures in java.lang.Throwable to their return types. The method signatures + * are not fully qualified (do not contain the declaring type), but its parameter types are fully + * qualified and contain spaces after each comma. + * + *

Note that Exception and Error do not have a separate map because they do not add any + * additional methods beyond those in Throwable. + */ + private static Map javaLangThrowableMethods; + static { primitives.add("int"); primitives.add("short"); @@ -198,6 +215,11 @@ private JavaLangUtils() { javaLangClassesAndInterfaces.add("VirtualMachineError"); javaLangClassesAndInterfaces.add("Void"); javaLangClassesAndInterfaces.add("WrongThreadException"); + Set withJavaLang = new HashSet<>(javaLangClassesAndInterfaces.size()); + for (String s : javaLangClassesAndInterfaces) { + withJavaLang.add("java.lang." + s); + } + javaLangClassesAndInterfaces.addAll(withJavaLang); // I made this list by going through the members of // java.lang and checking which were final classes. We @@ -223,11 +245,42 @@ private JavaLangUtils() { knownFinalJdkTypes.add("StringBuilder"); knownFinalJdkTypes.add("System"); knownFinalJdkTypes.add("Void"); - Set withJavaLang = new HashSet<>(knownFinalJdkTypes.size()); + withJavaLang = new HashSet<>(knownFinalJdkTypes.size()); for (String s : knownFinalJdkTypes) { withJavaLang.add("java.lang." + s); } knownFinalJdkTypes.addAll(withJavaLang); + + Map javaLangObjectMethods = new HashMap<>(); + javaLangObjectMethods.put("getClass()", "java.lang.Class"); + javaLangObjectMethods.put("toString()", "java.lang.String"); + javaLangObjectMethods.put("hashCode()", "int"); + javaLangObjectMethods.put("equals(java.lang.Object)", "boolean"); + javaLangObjectMethods.put("notifyAll()", "void"); + javaLangObjectMethods.put("notify()", "void"); + javaLangObjectMethods.put("wait()", "void"); + javaLangObjectMethods.put("wait(long)", "void"); + javaLangObjectMethods.put("wait(long, int)", "void"); + + JavaLangUtils.javaLangObjectMethods = Collections.unmodifiableMap(javaLangObjectMethods); + + Map javaLangThrowableMethods = new HashMap<>(); + // https://docs.oracle.com/javase/8/docs/api/java/lang/Throwable.html + javaLangThrowableMethods.put("addSuppressed()", "void"); + javaLangThrowableMethods.put("fillInStackTrace()", "java.lang.Throwable"); + javaLangThrowableMethods.put("getCause()", "java.lang.Throwable"); + javaLangThrowableMethods.put("getLocalizedMessage()", "java.lang.String"); + javaLangThrowableMethods.put("getMessage()", "java.lang.String"); + javaLangThrowableMethods.put("getStackTrace()", "java.lang.StackTraceElement[]"); + javaLangThrowableMethods.put("getSuppressed()", "java.lang.Throwable[]"); + javaLangThrowableMethods.put("initCause()", "java.lang.Throwable"); + javaLangThrowableMethods.put("printStackTrace()", "void"); + javaLangThrowableMethods.put("printStackTrace(java.io.PrintStream)", "void"); + javaLangThrowableMethods.put("printStackTrace(java.io.PrintWriter)", "void"); + javaLangThrowableMethods.put("setStackTrace(java.lang.StackTraceElement[])", "void"); + javaLangThrowableMethods.put("toString()", "java.lang.String"); + + JavaLangUtils.javaLangThrowableMethods = Collections.unmodifiableMap(javaLangThrowableMethods); } /** The integral primitives. */ @@ -273,46 +326,34 @@ private JavaLangUtils() { * @return the set of compatible types, such as ["boolean", "Boolean"] */ public static String[] getTypesForOp(String binOp) { - switch (binOp) { - case "*": - case "/": - case "%": - // JLS 15.17 - return NUMERIC_PRIMITIVES; - case "-": - // JLS 15.18 - return NUMERIC_PRIMITIVES; - case "+": - // JLS 15.18 (see note about "+", which can also mean string concatenation!) - return NUMERIC_PRIMITIVES_AND_STRING; - case ">>": - case ">>>": - case "<<": - // JSL 15.19 - return INTEGRAL_PRIMITIVES; - case "<": - case "<=": - case ">": - case ">=": - // JLS 15.20.1 - return NUMERIC_PRIMITIVES; - case "==": - case "!=": - // JLS 15.21 says that it's an error if one of the sides of an == or != is a boolean or - // numeric type, but the other is not. This return value is based on that error condition. - return NUMERIC_PRIMITIVES_AND_BOOLEANS; - case "^": - case "&": - case "|": - // JLS 15.22 - return NUMERIC_PRIMITIVES_AND_BOOLEANS; - case "||": - case "&&": - // JLS 15.23 and 15.24 - return BOOLEANS; - default: - throw new IllegalArgumentException("unexpected binary operator: " + binOp); - } + return switch (binOp) { + // JLS 15.17 + case "*", "/", "%" -> NUMERIC_PRIMITIVES; + + // JLS 15.18 + case "-" -> NUMERIC_PRIMITIVES; + + // JLS 15.18 (see note about "+", which can also mean string concatenation!) + case "+" -> NUMERIC_PRIMITIVES_AND_STRING; + + // JSL 15.19 + case ">>", ">>>", "<<" -> INTEGRAL_PRIMITIVES; + + // JLS 15.20.1 + case "<", "<=", ">", ">=" -> NUMERIC_PRIMITIVES; + + // JLS 15.21 says that it's an error if one of the sides of an == or != is a boolean or + // numeric type, but the other is not. This return value is based on that error condition. + case "==", "!=" -> NUMERIC_PRIMITIVES_AND_BOOLEANS; + + // JLS 15.22 + case "^", "&", "|" -> NUMERIC_PRIMITIVES_AND_BOOLEANS; + + // JLS 15.23 and 15.24 + case "||", "&&" -> BOOLEANS; + + default -> throw new IllegalArgumentException("unexpected binary operator: " + binOp); + }; } /** @@ -383,4 +424,37 @@ public static boolean inJdkPackage(String qualifiedName) { public static boolean isFinalJdkClass(String name) { return knownFinalJdkTypes.contains(name); } + + /** + * Checks if the given method signature is a method in java.lang.Object. For example, + * equals(java.lang.Object). + * + * @param methodSignature A method signature, not including the declaring type + * @return true if the method is defined in java.lang.Object + */ + public static boolean isJavaLangObjectMethod(String methodSignature) { + return javaLangObjectMethods.containsKey(methodSignature); + } + + /** + * Gets the map of methods in java.lang.Object, where the keys are method signatures (parameter + * types are fully qualified, with spaces after each comma, no declaring type) and the values are + * the return types of each method. + * + * @return A map of method signatures to their return types + */ + public static Map getJavaLangObjectMethods() { + return javaLangObjectMethods; + } + + /** + * Gets the map of methods in java.lang.Throwable, where the keys are method signatures (parameter + * types are fully qualified, with spaces after each comma, no declaring type) and the values are + * the return types of each method. + * + * @return A map of method signatures to their return types + */ + public static Map getJavaLangThrowableMethods() { + return javaLangThrowableMethods; + } } diff --git a/src/main/java/org/checkerframework/specimin/JavaParserUtil.java b/src/main/java/org/checkerframework/specimin/JavaParserUtil.java index ac912b130..fcf7dc260 100644 --- a/src/main/java/org/checkerframework/specimin/JavaParserUtil.java +++ b/src/main/java/org/checkerframework/specimin/JavaParserUtil.java @@ -1,31 +1,77 @@ package org.checkerframework.specimin; -import com.github.javaparser.StaticJavaParser; +import com.github.javaparser.ast.CompilationUnit; +import com.github.javaparser.ast.ImportDeclaration; import com.github.javaparser.ast.Node; import com.github.javaparser.ast.NodeList; import com.github.javaparser.ast.body.AnnotationDeclaration; +import com.github.javaparser.ast.body.BodyDeclaration; +import com.github.javaparser.ast.body.CallableDeclaration; import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration; import com.github.javaparser.ast.body.ConstructorDeclaration; +import com.github.javaparser.ast.body.EnumConstantDeclaration; import com.github.javaparser.ast.body.EnumDeclaration; import com.github.javaparser.ast.body.FieldDeclaration; import com.github.javaparser.ast.body.MethodDeclaration; +import com.github.javaparser.ast.body.Parameter; import com.github.javaparser.ast.body.TypeDeclaration; +import com.github.javaparser.ast.body.VariableDeclarator; import com.github.javaparser.ast.expr.ArrayInitializerExpr; +import com.github.javaparser.ast.expr.AssignExpr; import com.github.javaparser.ast.expr.Expression; +import com.github.javaparser.ast.expr.FieldAccessExpr; +import com.github.javaparser.ast.expr.LambdaExpr; +import com.github.javaparser.ast.expr.MethodCallExpr; +import com.github.javaparser.ast.expr.MethodReferenceExpr; +import com.github.javaparser.ast.expr.ObjectCreationExpr; +import com.github.javaparser.ast.expr.VariableDeclarationExpr; +import com.github.javaparser.ast.nodeTypes.NodeWithArguments; import com.github.javaparser.ast.nodeTypes.NodeWithDeclaration; +import com.github.javaparser.ast.nodeTypes.NodeWithExtends; +import com.github.javaparser.ast.nodeTypes.NodeWithImplements; +import com.github.javaparser.ast.nodeTypes.NodeWithTraversableScope; +import com.github.javaparser.ast.stmt.BlockStmt; +import com.github.javaparser.ast.stmt.ExplicitConstructorInvocationStmt; +import com.github.javaparser.ast.stmt.ReturnStmt; +import com.github.javaparser.ast.stmt.Statement; import com.github.javaparser.ast.type.ClassOrInterfaceType; import com.github.javaparser.ast.type.Type; +import com.github.javaparser.resolution.MethodUsage; +import com.github.javaparser.resolution.Resolvable; +import com.github.javaparser.resolution.TypeSolver; import com.github.javaparser.resolution.UnsolvedSymbolException; +import com.github.javaparser.resolution.declarations.AssociableToAST; +import com.github.javaparser.resolution.declarations.ResolvedMethodDeclaration; import com.github.javaparser.resolution.declarations.ResolvedMethodLikeDeclaration; +import com.github.javaparser.resolution.declarations.ResolvedParameterDeclaration; +import com.github.javaparser.resolution.declarations.ResolvedReferenceTypeDeclaration; +import com.github.javaparser.resolution.declarations.ResolvedTypeParameterDeclaration; +import com.github.javaparser.resolution.declarations.ResolvedTypeParameterDeclaration.Bound; +import com.github.javaparser.resolution.declarations.ResolvedValueDeclaration; +import com.github.javaparser.resolution.types.ResolvedLambdaConstraintType; import com.github.javaparser.resolution.types.ResolvedReferenceType; import com.github.javaparser.resolution.types.ResolvedType; +import com.github.javaparser.resolution.types.ResolvedWildcard; +import com.github.javaparser.symbolsolver.javaparsermodel.JavaParserFacade; +import com.github.javaparser.utils.Pair; import com.google.common.base.Splitter; +import java.util.AbstractMap; import java.util.ArrayList; +import java.util.Collection; import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Optional; +import java.util.Set; import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.checkerframework.checker.nullness.qual.EnsuresNonNull; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.checker.signature.qual.FullyQualifiedName; +import org.checkerframework.specimin.unsolved.SolvedMemberType; /** * A class containing useful static functions using JavaParser. @@ -43,6 +89,36 @@ private JavaParserUtil() { throw new UnsupportedOperationException("This class cannot be instantiated."); } + /** + * Set this TypeSolver instance to be used by the JavaParserFacade, so we don't have to find it + * through reflection in JavaParserSymbolSolver. This field is initialized once in SpeciminRunner. + * Note that by the time you evaluate the value of this field, it should already be non-null. + */ + private static @MonotonicNonNull TypeSolver typeSolver = null; + + /** + * Set the TypeSolver instance to be used by the JavaParserFacade. + * + * @param typeSolver the TypeSolver instance to set + */ + @EnsuresNonNull("JavaParserUtil.typeSolver") + public static void setTypeSolver(TypeSolver typeSolver) { + JavaParserUtil.typeSolver = typeSolver; + } + + /** + * Gets the type solver, and ensures it is non-null. + * + * @return The type solver + */ + public static TypeSolver getTypeSolver() { + if (typeSolver == null) { + throw new RuntimeException( + "TypeSolver is not set. Make sure to call setTypeSolver() in SpeciminRunner."); + } + return typeSolver; + } + /** * Removes a node from its compilation unit. If a node cannot be removed directly, it might be * wrapped inside another node, causing removal failure. This method iterates through the parent @@ -70,22 +146,53 @@ public static boolean isAClassPath(String potentialClassPath) { List elements = Splitter.onPattern("\\.").splitToList(potentialClassPath); int elementsCount = elements.size(); return elementsCount > 1 - && isCapital(elements.get(elementsCount - 1)) + && isAClassName(elements.get(elementsCount - 1)) // Classpaths cannot contain spaces! && elements.stream().noneMatch(s -> s.contains(" ")); } /** - * This method checks if a string is capitalized + * This method checks if a string represents a simple class name. * * @param string the string to be checked - * @return true if the string is capitalized + * @return true if the string represents a simple class name */ - public static boolean isCapital(String string) { + public static boolean isAClassName(String string) { + if (string.contains(".")) { + return false; + } + + // A class name should have its first letter capitalized but its second letter + // should be lower case. If otherwise, then it may be a constant Character first = string.charAt(0); + if (string.length() > 1) { + Character second = string.charAt(1); + + return Character.isUpperCase(first) && Character.isLowerCase(second); + } + + // A name like "A": assume it's a class return Character.isUpperCase(first); } + /** + * Returns true if a simple name looks like a constant; i.e., all its characters are either + * capital or _. This is a heuristic. + * + * @param simpleName The simple name to check, should contain no dots + * @return If it looks like a constant + */ + public static boolean looksLikeAConstant(String simpleName) { + for (int i = 0; i < simpleName.length(); i++) { + char character = simpleName.charAt(i); + if (!Character.isUpperCase(character) && character != '_') { + return false; + } + } + + return true; + } + /** * Utility method to check if the given declaration is a local class declaration. * @@ -118,7 +225,7 @@ public static ResolvedReferenceType classOrInterfaceTypeToResolvedReferenceType( } /** - * Erases type arguments from a method signature string. + * Erases type arguments from a method or type signature string. * * @param signature the signature * @return the same signature without type arguments @@ -196,21 +303,7 @@ public static String getValueTypeFromAnnotationExpression(Expression value) { */ @SuppressWarnings("signature") // result is a fully-qualified name or else this throws public static @FullyQualifiedName String getEnclosingClassName(Node node) { - Node parent = getEnclosingClassLike(node); - - if (parent instanceof ClassOrInterfaceDeclaration) { - return ((ClassOrInterfaceDeclaration) parent).getFullyQualifiedName().orElseThrow(); - } - - if (parent instanceof EnumDeclaration) { - return ((EnumDeclaration) parent).getFullyQualifiedName().orElseThrow(); - } - - if (parent instanceof AnnotationDeclaration) { - return ((AnnotationDeclaration) parent).getFullyQualifiedName().orElseThrow(); - } - - throw new RuntimeException("unexpected kind of node: " + parent.getClass()); + return getEnclosingClassLike(node).getFullyQualifiedName().orElseThrow(); } /** @@ -222,54 +315,52 @@ public static String getValueTypeFromAnnotationExpression(Expression value) { * @param node a node that is contained in a class-like structure * @return the nearest enclosing class-like node */ - public static Node getEnclosingClassLike(Node node) { + public static TypeDeclaration getEnclosingClassLike(Node node) { Node parent = node.getParentNode().orElseThrow(); - while (!(parent instanceof ClassOrInterfaceDeclaration - || parent instanceof EnumDeclaration - || parent instanceof AnnotationDeclaration)) { + while (!(parent instanceof TypeDeclaration)) { parent = parent.getParentNode().orElseThrow(); } - return parent; + return (TypeDeclaration) parent; } /** - * Given a String of types (separated by commas), return a List of these types, with any - * primitives converted to their object counterparts. Use this instead of {@code .split(", ")} to - * properly handle generics. + * See {@link #getEnclosingClassLike(Node)} for more details. This does not throw if a parent is + * not found; instead, it returns null. * - * @param commaSeparatedTypes A string of comma separated types - * @return a list of strings representing the types in commaSeparatedTypes + * @param node a node that is contained in a class-like structure + * @return the nearest enclosing class-like node */ - public static List getReferenceTypesFromCommaSeparatedString(String commaSeparatedTypes) { - if (commaSeparatedTypes == null || commaSeparatedTypes.isBlank()) { - return Collections.EMPTY_LIST; - } - - // Splitting them is simply to change primitives to objects so we do not - // get an error when parsing in StaticJavaParser (note that this array) - // may contain incomplete types like ["Map"] - String[] tokens = commaSeparatedTypes.split(","); - - for (int i = 0; i < tokens.length; i++) { - if (JavaLangUtils.isPrimitive(tokens[i].trim())) { - tokens[i] = JavaLangUtils.getPrimitiveAsBoxedType(tokens[i].trim()); - } + public static @Nullable TypeDeclaration getEnclosingClassLikeOptional(Node node) { + try { + return getEnclosingClassLike(node); + } catch (Exception ex) { + return null; } + } - // Parse as a generic type, then get the type arguments - // This way we can properly differentiate between commas within type arguments - // versus actual commas in javac error messages - Type parsed = StaticJavaParser.parseType("ToParse<" + String.join(", ", tokens) + ">"); + /** + * Gets the super class of a node. If one does not exist, this method will throw. + * + * @param node The node to find the super class of + * @return The super class + */ + public static ClassOrInterfaceType getSuperClass(Node node) { + TypeDeclaration decl = getEnclosingClassLike(node); - List types = new ArrayList<>(); - NodeList typeArguments = parsed.asClassOrInterfaceType().getTypeArguments().orElse(null); + return decl.asClassOrInterfaceDeclaration().getExtendedTypes().get(0); + } - if (typeArguments != null) { - for (Type typeArgument : typeArguments) { - types.add(typeArgument.toString()); - } - } - return types; + /** + * Given a qualified class name, return the simple name. i.e., org.example.ClassName --> + * ClassName. + * + *

This is also safe to call on non qualified names as well; it simply returns the input. + * + * @param qualified The qualified class name + * @return The simple class name + */ + public static String getSimpleNameFromQualifiedName(String qualified) { + return qualified.substring(qualified.lastIndexOf('.') + 1); } /** @@ -373,4 +464,2248 @@ static String removeMethodReturnTypeAndAnnotationsImpl(String declAsString) { String result = methodWithoutReturnType.replace("< ", "<"); return result; } + + /** + * Returns the FQN if the expression is a reference to a static method or field or null if it + * isn't one. This method is intended to be used with unsolvable expressions, with which it should + * always return the correct result. + * + * @param expr The expression + * @return The FQN if it is a static member, empty otherwise + */ + public static @Nullable String getFQNIfStaticMember(Expression expr) { + CompilationUnit cu = expr.findCompilationUnit().get(); + + String nameOfExpr; + String nameOfScope = null; + Expression scope = null; + + if (expr.isNameExpr()) { + nameOfScope = expr.asNameExpr().getNameAsString(); + nameOfExpr = expr.asNameExpr().getNameAsString(); + + // If an expression is a class name, it cannot be a static member + if (isAClassName(nameOfExpr)) { + return null; + } + } else if (expr.isMethodCallExpr()) { + nameOfExpr = expr.asMethodCallExpr().getNameAsString(); + if (expr.asMethodCallExpr().hasScope()) { + scope = expr.asMethodCallExpr().getScope().get(); + } else { + nameOfScope = nameOfExpr; + } + } else if (expr.isFieldAccessExpr()) { + nameOfExpr = expr.asFieldAccessExpr().getNameAsString(); + scope = expr.asFieldAccessExpr().getScope(); + } else { + return null; + } + + if (scope != null) { + if (scope.isNameExpr()) { + try { + ResolvedType scopeType = expr.calculateResolvedType(); + + if (getSimpleNameFromQualifiedName(scopeType.describe()).equals(scope.toString())) { + return scopeType.describe() + "." + nameOfExpr; + } + } catch (UnsolvedSymbolException ex) { + // continue + } + + nameOfScope = scope.asNameExpr().getNameAsString(); + } else if (scope.isFieldAccessExpr()) { + try { + ResolvedType scopeType = expr.calculateResolvedType(); + + if (getSimpleNameFromQualifiedName(scopeType.describe()).endsWith(scope.toString())) { + return scopeType.describe() + "." + nameOfExpr; + } + } catch (UnsolvedSymbolException ex) { + // continue + } + + nameOfScope = scope.asFieldAccessExpr().toString(); + if (isAClassPath(nameOfScope)) { + return nameOfScope + "." + nameOfExpr; + } + } else { + return null; + } + } + + if (nameOfScope == null) { + return null; + } + + for (ImportDeclaration importDecl : cu.getImports()) { + // A static member can either be imported as a static method/field, like + // import static org.example.SomeClass.fieldName; + if (importDecl.isStatic() && importDecl.getNameAsString().endsWith("." + nameOfExpr)) { + return importDecl.getNameAsString(); + } + + // A static member can also be found if its scope is a non-static, imported type, + // i.e., Foo.myField, if there is also import org.example.Foo; + if (!importDecl.isStatic() && importDecl.getNameAsString().endsWith("." + nameOfScope)) { + return expr.isNameExpr() + ? importDecl.getNameAsString() + : importDecl.getNameAsString() + "." + nameOfExpr; + } + } + + // The scope may also be a simple class name located in the same package + if (scope != null && isAClassName(nameOfScope)) { + return cu.getPackageDeclaration().get().getNameAsString() + + "." + + nameOfScope + + "." + + nameOfExpr; + } + + return null; + } + + /** + * Returns true if the expression type is resolvable; i.e., {@code calculateResolvedType()} runs + * without an {@code UnsolvedSymbolException}. + * + * @param expr The expression + * @return True if the expression is resolvable + */ + public static boolean isExprTypeResolvable(Expression expr) { + try { + expr.calculateResolvedType(); + return true; + } catch (UnsolvedSymbolException | UnsupportedOperationException ex) { + // We can get an UnsupportedOperationException when trying to resolve an unsolvable method + // reference + return false; + } + } + + /** + * Returns true if this expression is resolvable to a definition. + * + * @param expr The expression to resolve + * @return True if this expression is of type {@code Resolvable} and also has a resolvable + * definition + */ + public static boolean isExprDefinitionResolvable(Expression expr) { + if (!(expr instanceof Resolvable resolvable)) { + return false; + } + + try { + resolvable.resolve(); + return true; + } catch (UnsolvedSymbolException ex) { + return false; + } catch (UnsupportedOperationException ex) { + if (tryFindCorrespondingDeclarationForConstraintQualifiedExpression(expr) != null) { + return true; + } + return false; + } + } + + /** + * Gets the type from a {@code ResolvedValueDeclaration}. Returns null if unable to be found. + * + * @param resolved The resolved value declaration + * @param fqnToCompilationUnits The map of FQNs to compilation units + * @return The Type of the resolved value declaration + */ + public static @Nullable Type getTypeFromResolvedValueDeclaration( + ResolvedValueDeclaration resolved, Map fqnToCompilationUnits) { + Node attached = tryFindAttachedNode(resolved, fqnToCompilationUnits); + + if (attached instanceof VariableDeclarationExpr varDecl) { + Type type = varDecl.getElementType(); + + if (!type.isVarType()) { + return type; + } + + // var can only have one variable + return tryGetTypeFromExpression( + varDecl.getVariables().get(0).getInitializer().get(), fqnToCompilationUnits); + } else if (attached instanceof VariableDeclarator varDecl) { + Type type = varDecl.getType(); + + if (!type.isVarType()) { + return type; + } + + // var can only have one variable + return tryGetTypeFromExpression(varDecl.getInitializer().get(), fqnToCompilationUnits); + } else if (attached instanceof FieldDeclaration fieldDecl) { + return fieldDecl.getElementType(); + } else if (attached instanceof Parameter param) { + return param.getType(); + } + + return null; + } + + /** + * Tries to get the type from an expression; useful for var types. If a type cannot be found, this + * method returns null. + * + * @param expression The expression to get the type from + * @return The type of the expression, or null if it cannot be found + */ + private static @Nullable Type tryGetTypeFromExpression( + Expression expression, Map fqnToCompilationUnits) { + try { + if (expression.isNameExpr()) { + ResolvedValueDeclaration resolved = expression.asNameExpr().resolve(); + + return getTypeFromResolvedValueDeclaration(resolved, fqnToCompilationUnits); + } else if (expression.isFieldAccessExpr()) { + ResolvedValueDeclaration resolved = expression.asFieldAccessExpr().resolve(); + + return getTypeFromResolvedValueDeclaration(resolved, fqnToCompilationUnits); + } else if (expression.isMethodCallExpr()) { + ResolvedMethodDeclaration resolved = expression.asMethodCallExpr().resolve(); + + if (resolved.toAst().isPresent()) { + MethodDeclaration methodDecl = (MethodDeclaration) resolved.toAst().get(); + return methodDecl.getType(); + } + } + } catch (UnsolvedSymbolException ex) { + return null; + } + + if (expression.isArrayCreationExpr()) { + return expression.asArrayCreationExpr().createdType(); + } else if (expression.isCastExpr()) { + return expression.asCastExpr().getType(); + } + + return null; + } + + /** + * Tries to get the type from an expression; useful for var types. If a type cannot be found, this + * method returns null. This method is similar to {@link #tryGetTypeFromExpression(Expression, + * Map)}, but this accounts for slightly more cases when a String version of the type can be + * returned, but a corresponding Type in the AST cannot be found. + * + * @param expression The expression to get the type from + * @param fqnToCompilationUnits The map of FQNs to compilation units + * @return The type of the expression, or null if it cannot be found + */ + public static @Nullable String tryGetTypeAsStringFromExpression( + Expression expression, Map fqnToCompilationUnits) { + try { + return expression.calculateResolvedType().describe(); + } catch (UnsolvedSymbolException ex) { + // continue + } + + Type type = tryGetTypeFromExpression(expression, fqnToCompilationUnits); + + if (type != null) { + return type.toString(); + } + + if (expression.isClassExpr()) { + return "java.lang.Class<" + expression.asClassExpr().getTypeAsString() + ">"; + } else if (expression.isInstanceOfExpr()) { + return "boolean"; + } + + return null; + } + + /** + * Removes array brackets from a type name. i.e., int[][][] -> int + * + * @param name The name of the type + * @return The name of the type, without the array brackets + */ + public static String removeArrayBrackets(String name) { + return name.replaceAll("(\\[\\])+$", ""); + } + + /** + * Counts the number of array brackets in a type. If the type is int[][][], this method returns 3. + * + * @param name The name of the type + * @return The number of array brackets in the type + */ + public static int countNumberOfArrayBrackets(String name) { + int count = 0; + + for (int i = name.length() - 1; + i > 0 && name.charAt(i) == ']' && name.charAt(i - 1) == '['; + i -= 2) { + count++; + } + + return count; + } + + /** + * When getting the scope/children of a ClassOrInterfaceType, it returns another + * ClassOrInterfaceType. However, it does not differentiate between whether this type is a package + * or if it's another type, so this method helps to differentiate between the two. + * + * @param type The type + * @return True if the type is probably a package, based on conventions + */ + public static boolean isProbablyAPackage(ClassOrInterfaceType type) { + if (type.getTypeArguments().isPresent()) { + return false; + } + + return isProbablyAPackage(type.getNameAsString()); + } + + /** + * Returns true if the given string is probably a package name. This is a heuristic based on + * common Java package naming conventions. If the first character and each character after a dot + * are all lowercase, then this is probably a package. + * + * @param type The type/package name + * @return True if the type is probably a package + */ + public static boolean isProbablyAPackage(String type) { + for (String segment : type.split("\\.", -1)) { + if (Character.isUpperCase(segment.charAt(0))) { + return false; + } + } + return true; + } + + /** + * Checks to see if an expression (FieldAccessExpr or NameExpr) is likely part of a package. This + * checks parents too, so org.example would be seen as org.example.Test, allowing us to + * differentiate between part of a package and a field name. + * + * @param type The type + * @return True if the type is probably a package, based on conventions + */ + public static boolean isProbablyAPackage(Expression type) { + if (!type.isFieldAccessExpr() && !type.isNameExpr()) { + return false; + } + + // Baz in Baz.myField + if (type.isNameExpr() && isAClassName(type.toString())) { + return false; + } + + if (type.isFieldAccessExpr() && isAClassPath(type.toString())) { + return false; + } + + while (type.hasParentNode() && type.getParentNode().get() instanceof FieldAccessExpr field) { + type = field; + if (isAClassPath(type.toString())) { + return true; + } + } + + return false; + } + + /** + * Returns a list of existing callable declarations (methods, constructors, or enum constants) + * that match the given node with arguments, even if some arguments are unresolvable. + * + * @param withArgs The node representing a method call, constructor call, or enum constant + * declaration with arguments + * @param fqnsToCompilationUnits The map of fully qualified names to their compilation units + * @return A list of matching {@link CallableDeclaration} instances, or an empty list if none are + * found + */ + public static List> tryResolveNodeWithUnresolvableArguments( + NodeWithArguments withArgs, Map fqnsToCompilationUnits) { + if (withArgs instanceof MethodCallExpr parentMethodCall) { + return tryResolveMethodCallWithUnresolvableArguments( + parentMethodCall, fqnsToCompilationUnits); + } else if (withArgs instanceof ExplicitConstructorInvocationStmt parentConstructorCall) { + return tryResolveConstructorCallWithUnresolvableArguments( + parentConstructorCall, fqnsToCompilationUnits); + } else if (withArgs instanceof ObjectCreationExpr parentConstructorCall) { + return tryResolveConstructorCallWithUnresolvableArguments( + parentConstructorCall, fqnsToCompilationUnits); + } else if (withArgs instanceof EnumConstantDeclaration parentEnumConstantDeclaration) { + return tryResolveEnumConstantDeclarationWithUnresolvableArguments( + parentEnumConstantDeclaration, fqnsToCompilationUnits); + } else { + // Not possible: + // https://javadoc.io/doc/com.github.javaparser/javaparser-core/latest/com/github/javaparser/ast/nodeTypes/NodeWithArguments.html + throw new RuntimeException("Unexpected NodeWithArguments type: " + withArgs.getClass()); + } + } + + /** + * Tries to resolve a node with unresolvable arguments. If no single callable can be found, it + * returns null. + * + * @param node The node with arguments to resolve + * @param fqnsToCompilationUnits The map of fully qualified names to their compilation units + * @return A resolvable callable if it can be found, null otherwise + */ + public static @Nullable CallableDeclaration + tryFindSingleCallableForNodeWithUnresolvableArguments( + NodeWithArguments node, Map fqnsToCompilationUnits) { + List> callables = + tryResolveNodeWithUnresolvableArguments(node, fqnsToCompilationUnits); + if (callables.isEmpty()) { + return null; + } + + if (callables.size() == 1) { + return callables.get(0); + } + + List<@Nullable Object> argumentTypes = + new ArrayList<>(getArgumentTypesAsResolved(node.getArguments())); + + for (int i = 0; i < argumentTypes.size(); i++) { + if (argumentTypes.get(i) == null) { + argumentTypes.set( + i, tryGetTypeAsStringFromExpression(node.getArgument(i), fqnsToCompilationUnits)); + } + } + + // If there is only one callable where the rest of the parameters match and a few others that + // are null, return this maybe best match + CallableDeclaration maybeBestMatch = null; + // If there are multiple callables, find the best match, i.e., FQNs = FQNs and simple names = + // simple names + // If there is one that matches exactly, return it; others that match it directly are simply + // overrides + for (CallableDeclaration callable : callables) { + boolean isAMatch = true; + int nulls = 0; + for (int i = 0; i < callable.getParameters().size(); i++) { + Object typeInCall = argumentTypes.get(i); + + if (typeInCall == null) { + nulls++; + continue; + } + + // The call to tryResolve... already guarantees that ResolvedType is handled correctly + if (typeInCall instanceof ResolvedType) { + continue; + } + + Type paramType = callable.getParameter(i).getType(); + if (typeInCall instanceof String typeAsString) { + // If the type in the call is a string, it must match the parameter type exactly + if (!erase(paramType.asString()).equals(erase(typeAsString))) { + isAMatch = false; + break; + } + } + } + + if (isAMatch) { + if (nulls == 0) { + // If there are no nulls, this is a perfect match + return callable; + } else if (maybeBestMatch == null) { + // If there are nulls, this is the best match so far + maybeBestMatch = callable; + } else { + // If nulls > 0 and maybeBestMatch is already existing, return null since we have + // ambiguities: handle in UnsolvedSymbolGenerator + return null; + } + } + } + + return maybeBestMatch; + } + + /** + * Given a constructor call, returns all possible constructors that match the arity and known + * types of the arguments. This returns an empty list if no matching constructors were found or if + * the declaring type could not be solved. + * + * @param constructorCall The constructor call expression + * @param fqnToCompilationUnits The map of type FQNs to their compilation units + * @return All possible constructor declarations + */ + private static List tryResolveConstructorCallWithUnresolvableArguments( + ObjectCreationExpr constructorCall, Map fqnToCompilationUnits) { + List<@Nullable ResolvedType> parameterTypes = + getArgumentTypesAsResolved(constructorCall.getArguments()); + + TypeDeclaration enclosingClass; + + try { + ResolvedType type = constructorCall.getType().resolve(); + + enclosingClass = getTypeFromQualifiedName(type.describe(), fqnToCompilationUnits); + + if (enclosingClass == null) { + return List.of(); + } + } catch (UnsolvedSymbolException ex) { + // not relevant + return List.of(); + } + + List candidates = new ArrayList<>(); + + addAllMatchingCallablesToList( + enclosingClass, parameterTypes, candidates, null, ConstructorDeclaration.class); + + return candidates; + } + + /** + * Given a constructor call, returns all possible constructors that match the arity and known + * types of the arguments. This returns an empty list if no matching constructors were found or if + * the declaring type could not be solved. + * + * @param constructorCall The constructor call statement (super or this constructor call) + * @param fqnToCompilationUnits The map of type FQNs to their compilation units + * @return All possible constructor declarations + */ + private static List tryResolveConstructorCallWithUnresolvableArguments( + ExplicitConstructorInvocationStmt constructorCall, + Map fqnToCompilationUnits) { + List<@Nullable ResolvedType> parameterTypes = + getArgumentTypesAsResolved(constructorCall.getArguments()); + + TypeDeclaration enclosingClass = getEnclosingClassLike(constructorCall); + List candidates = new ArrayList<>(); + + if (constructorCall.isThis()) { + addAllMatchingCallablesToList( + enclosingClass, parameterTypes, candidates, null, ConstructorDeclaration.class); + } else { + TypeDeclaration parent = null; + try { + parent = + getTypeFromQualifiedName( + getSuperClass(constructorCall).resolve().describe(), fqnToCompilationUnits); + } catch (UnsolvedSymbolException ex) { + // continue + } + + if (parent != null) { + addAllMatchingCallablesToList( + parent, parameterTypes, candidates, null, ConstructorDeclaration.class); + } + } + + return candidates; + } + + /** + * Given a method call, returns all possible methods that match the arity and known types of the + * arguments. This returns an empty list if no matching methods were found or if the declaring + * type could not be solved. + * + * @param methodCall The method call expression + * @param fqnToCompilationUnits The map of type FQNs to their compilation units + * @return All possible method declarations + */ + private static List tryResolveMethodCallWithUnresolvableArguments( + MethodCallExpr methodCall, Map fqnToCompilationUnits) { + boolean isSuperOnly = false; + + List> enclosingClass = new ArrayList<>(); + if (methodCall.hasScope()) { + Expression scope = methodCall.getScope().get(); + + if (scope.isSuperExpr()) { + isSuperOnly = true; + } + + try { + ResolvedType scopeType = scope.calculateResolvedType(); + + if (scopeType.isTypeVariable()) { + for (Bound bound : scopeType.asTypeParameter().getBounds()) { + TypeDeclaration decl = + getTypeFromQualifiedName(bound.getType().describe(), fqnToCompilationUnits); + + if (decl != null) { + enclosingClass.add(decl); + } + } + + if (enclosingClass.isEmpty()) { + return List.of(); + } + } else { + TypeDeclaration decl = + getTypeFromQualifiedName(scopeType.describe(), fqnToCompilationUnits); + + if (decl == null) { + return List.of(); + } + + enclosingClass.add(decl); + } + } catch (UnsolvedSymbolException ex) { + // Maybe the scope has type arguments; try to resolve without those. + String scopeType = + getQualifiedNameOfTypeOfExpressionWithUnresolvableTypeArgs( + scope, fqnToCompilationUnits); + + if (scopeType == null) { + return List.of(); + } + + TypeDeclaration decl = getTypeFromQualifiedName(scopeType, fqnToCompilationUnits); + + if (decl == null) { + return List.of(); + } + + enclosingClass.add(decl); + } + } else { + enclosingClass.add(getEnclosingClassLike(methodCall)); + } + + List<@Nullable ResolvedType> parameterTypes = + getArgumentTypesAsResolved(methodCall.getArguments()); + + List candidates = new ArrayList<>(); + + for (TypeDeclaration typeDecl : enclosingClass) { + if (!isSuperOnly) { + addAllMatchingCallablesToList( + typeDecl, + parameterTypes, + candidates, + methodCall.getNameAsString(), + MethodDeclaration.class); + } + + for (TypeDeclaration ancestor : getAllSolvableAncestors(typeDecl, fqnToCompilationUnits)) { + addAllMatchingCallablesToList( + ancestor, + parameterTypes, + candidates, + methodCall.getNameAsString(), + MethodDeclaration.class); + } + } + + return candidates; + } + + /** + * Given an enum constant declaration, returns all possible constructors that match the arity and + * known types of the arguments. This returns an empty list if no matching constructors were found + * or if the declaring type could not be solved. + * + * @param enumConstant The enum constant declaration + * @param fqnToCompilationUnits The map of type FQNs to their compilation units + * @return All possible constructor declarations + */ + private static List + tryResolveEnumConstantDeclarationWithUnresolvableArguments( + EnumConstantDeclaration enumConstant, + Map fqnToCompilationUnits) { + List<@Nullable ResolvedType> parameterTypes = + getArgumentTypesAsResolved(enumConstant.getArguments()); + + TypeDeclaration enclosingClass; + + try { + ResolvedType type = enumConstant.resolve().getType(); + + enclosingClass = getTypeFromQualifiedName(type.describe(), fqnToCompilationUnits); + + if (enclosingClass == null) { + return List.of(); + } + } catch (UnsolvedSymbolException ex) { + // not relevant + return List.of(); + } + + List candidates = new ArrayList<>(); + + addAllMatchingCallablesToList( + enclosingClass, parameterTypes, candidates, null, ConstructorDeclaration.class); + + return candidates; + } + + /** + * Given an expression that has a resolvable definition, return the FQN of its resolved type if + * .calculateResolvedType() fails because of unresolvable type arguments. Returns the string + * instead of the ResolvedType to prevent unintentional use of the resulting ResolvedType, which + * uses a detached node. + * + * @param expr The expression whose type needs to be resolved + * @param fqnToCompilationUnits The map of FQNs to compilation units + * @return The FQN of the resolved type if found, null otherwise + */ + public static @Nullable String getQualifiedNameOfTypeOfExpressionWithUnresolvableTypeArgs( + Expression expr, Map fqnToCompilationUnits) { + if (!(expr instanceof Resolvable resolvable)) { + return null; + } + Object resolved = null; + + try { + resolved = resolvable.resolve(); + } catch (UnsolvedSymbolException ex) { + return null; + } + + ClassOrInterfaceType classOrInterfaceType = null; + if (resolved instanceof ResolvedValueDeclaration resolvedValueDecl) { + Type type = getTypeFromResolvedValueDeclaration(resolvedValueDecl, fqnToCompilationUnits); + + if (type != null && type.isClassOrInterfaceType()) { + classOrInterfaceType = type.asClassOrInterfaceType(); + } + } else if (resolved instanceof ResolvedMethodDeclaration resolvedMethodDeclaration) { + MethodDeclaration method = + (MethodDeclaration) tryFindAttachedNode(resolvedMethodDeclaration, fqnToCompilationUnits); + + if (method != null) { + Type type = method.getType(); + if (type != null && type.isClassOrInterfaceType()) { + classOrInterfaceType = type.asClassOrInterfaceType(); + } + } + } + + if (classOrInterfaceType == null) { + return null; + } + + // I tried cloning the type and temporarily adding it to the compilation unit, but doing so + // prevents it from being removed, which did change the output of some test cases. We'll + // do this instead, but it's definitely not pretty. + Optional> typeArgs = classOrInterfaceType.getTypeArguments(); + classOrInterfaceType.removeTypeArguments(); + + try { + ResolvedType resolvedType = classOrInterfaceType.resolve(); + if (typeArgs.isPresent()) { + classOrInterfaceType.setTypeArguments(typeArgs.get()); + } + return resolvedType.describe(); + } catch (UnsolvedSymbolException ex2) { + if (typeArgs.isPresent()) { + classOrInterfaceType.setTypeArguments(typeArgs.get()); + } + return null; + } + } + + /** + * Helper method for {@link #tryResolveConstructorCallWithUnresolvableArguments}. Gets argument + * types as their resolved counterparts and null if unresolvable. + * + * @param arguments The arguments, as a list of expressions + * @return The list of resolved types, or null if unresolvable + */ + public static List<@Nullable ResolvedType> getArgumentTypesAsResolved( + List arguments) { + List<@Nullable ResolvedType> parameterTypes = new ArrayList<>(); + + for (Expression argument : arguments) { + try { + parameterTypes.add(argument.calculateResolvedType()); + } catch (UnsolvedSymbolException ex) { + parameterTypes.add(null); + } + } + + return parameterTypes; + } + + /** + * Helper method for {@link #tryResolveConstructorCallWithUnresolvableArguments} and {@link + * #tryResolveMethodCallWithUnresolvableArguments}. Adds all callables (constructors/methods) that + * match the given parameterTypes to the output list. + * + * @param typeDecl The type declaration to search through + * @param parameterTypes The resolved parameter types. Fully qualified names if resolvable, simple + * names if not, and null if no type could be found at all. + * @param result The list to append to + * @param methodName The method name, if the callable is a method (it is ignored otherwise) + * @param callableType The type of callable (i.e., ConstructorDeclaration or MethodDeclaration) + */ + private static > void addAllMatchingCallablesToList( + TypeDeclaration typeDecl, + List<@Nullable ResolvedType> parameterTypes, + List result, + @Nullable String methodName, + Class callableType) { + List> callables; + if (callableType == ConstructorDeclaration.class) { + callables = typeDecl.getConstructors(); + } else if (callableType == MethodDeclaration.class) { + callables = typeDecl.getMethods(); + } else { + // Impossible: see + // https://www.javadoc.io/doc/com.github.javaparser/javaparser-core/latest/com/github/javaparser/ast/body/CallableDeclaration.html + throw new IllegalArgumentException("Impossible CallableDeclaration type."); + } + + for (CallableDeclaration callable : callables) { + if (callable.getParameters().size() != parameterTypes.size()) { + continue; + } + if (callableType == MethodDeclaration.class + && !callable.getNameAsString().equals(methodName)) { + continue; + } + + boolean isAMatch = true; + + for (int i = 0; i < callable.getParameters().size(); i++) { + ResolvedType typeInCall = parameterTypes.get(i); + + try { + ResolvedType resolvedParameterType = callable.getParameter(i).resolve().getType(); + + if (typeInCall == null) { + continue; + } + + if (resolvedParameterType.isReferenceType() && typeInCall.isReferenceType()) { + if (!resolvedParameterType.isAssignableBy(typeInCall)) { + isAMatch = false; + break; + } + } + } catch (UnsolvedSymbolException ex) { + if (typeInCall != null) { + isAMatch = false; + break; + } + } + } + + if (isAMatch) { + result.add(callableType.cast(callable)); + } + } + } + + /** + * Utility method to get the extended/implemented types from a type declaration. + * + * @param typeDecl The type declaration + * @return A list of direct super types (extended/implemented) + */ + public static List getDirectSuperTypes(TypeDeclaration typeDecl) { + List extendedOrImplemented = new ArrayList<>(); + + if (typeDecl instanceof NodeWithExtends withExtends) { + extendedOrImplemented.addAll(withExtends.getExtendedTypes()); + } + if (typeDecl instanceof NodeWithImplements withImplements) { + extendedOrImplemented.addAll(withImplements.getImplementedTypes()); + } + + return extendedOrImplemented; + } + + /** + * Finds all unsolvable ancestors, given a type declaration to start. + * + * @param start The type declaration + * @param fqnToCompilationUnits A map of FQNs to compilation units + * @return A list of type declarations representing all unsolvable ancestors + */ + public static List getAllUnsolvableAncestors( + TypeDeclaration start, Map fqnToCompilationUnits) { + List result = new ArrayList<>(); + + getAllUnsolvableAncestorsImpl(start, fqnToCompilationUnits, result); + + return result; + } + + /** + * Helper method for {@link #getAllUnsolvableAncestors(TypeDeclaration, Map)}. Recursively calls + * itself on all unsolvable ancestors. + * + * @param start The declaration to start at + * @param fqnToCompilationUnits The map of FQNs to compilation units + * @param result The + */ + private static void getAllUnsolvableAncestorsImpl( + TypeDeclaration start, + Map fqnToCompilationUnits, + List result) { + List extendedOrImplemented = getDirectSuperTypes(start); + + for (ClassOrInterfaceType type : extendedOrImplemented) { + try { + ResolvedType resolvedType = type.resolve(); + TypeDeclaration typeDecl = + getTypeFromQualifiedName(resolvedType.describe(), fqnToCompilationUnits); + + if (typeDecl == null) { + continue; + } + + getAllUnsolvableAncestorsImpl(typeDecl, fqnToCompilationUnits, result); + } catch (UnsolvedSymbolException ex) { + result.add(type); + } + } + } + + /** + * Finds all solvable ancestors (in JDK and user-defined types), given a type declaration to + * start. + * + * @param start The type declaration + * @return A set of resolved type declarations representing all solvable ancestors + */ + public static Set getAllJDKAncestors(TypeDeclaration start) { + Set result = new HashSet<>(); + + try { + ResolvedReferenceTypeDeclaration resolved = start.resolve(); + getAllJDKAncestorsImpl(resolved, result); + } catch (UnsolvedSymbolException ex) { + // continue + } + + return result; + } + + /** + * Finds all JDK ancestors of a given type recursively. + * + * @param type The type declaration + * @param result The set to append to + */ + private static void getAllJDKAncestorsImpl( + ResolvedReferenceTypeDeclaration type, Set result) { + for (ResolvedReferenceType ancestor : type.getAncestors(true)) { + if (JavaLangUtils.inJdkPackage(ancestor.getQualifiedName())) { + result.add(ancestor.getTypeDeclaration().get()); + } + + getAllJDKAncestorsImpl(ancestor.getTypeDeclaration().get(), result); + } + } + + /** + * Finds all solvable non-JDK ancestors, given a type declaration to start. + * + * @param start The type declaration + * @param fqnToCompilationUnits A map of FQNs to compilation units + * @return A list of type declarations representing all solvable ancestors + */ + public static List> getAllSolvableAncestors( + TypeDeclaration start, Map fqnToCompilationUnits) { + List> result = new ArrayList<>(); + + getAllSolvableAncestorsImpl(start, fqnToCompilationUnits, result); + + return result; + } + + /** + * Helper method for {@link #getAllSolvableAncestors(TypeDeclaration, Map)}. Recursively calls + * itself on all solvable ancestors. + * + * @param start The declaration to start at + * @param fqnToCompilationUnits The map of FQNs to compilation units + * @param result The + */ + private static void getAllSolvableAncestorsImpl( + TypeDeclaration start, + Map fqnToCompilationUnits, + List> result) { + List extendedOrImplemented = getDirectSuperTypes(start); + + for (ClassOrInterfaceType type : extendedOrImplemented) { + try { + ResolvedType resolvedType = type.resolve(); + TypeDeclaration typeDecl = + getTypeFromQualifiedName(resolvedType.describe(), fqnToCompilationUnits); + + if (typeDecl == null) { + continue; + } + + result.add(typeDecl); + getAllSolvableAncestorsImpl(typeDecl, fqnToCompilationUnits, result); + } catch (UnsolvedSymbolException ex) { + // continue + } + } + } + + /** + * Gets the corresponding type declaration from a qualified type name. Use this method instead of + * casting from toAst() to avoid resolve() errors on child nodes. + * + * @param fqn The fully-qualified type name; no need to erase because this method does it. + * @param fqnToCompilationUnits A map of fully-qualified type names to their compilation units + * @return The type declaration; null if not in the project. + */ + public static @Nullable TypeDeclaration getTypeFromQualifiedName( + String fqn, Map fqnToCompilationUnits) { + + String erased = erase(fqn); + String searchFQN = erased; + + CompilationUnit someCandidate = fqnToCompilationUnits.get(searchFQN); + while (searchFQN.contains(".") && someCandidate == null) { + searchFQN = searchFQN.substring(0, searchFQN.lastIndexOf('.')); + someCandidate = fqnToCompilationUnits.get(searchFQN); + } + + if (someCandidate == null) { + // Not in project; solved by reflection, not our concern + return null; + } + + TypeDeclaration type = + someCandidate + .findFirst( + TypeDeclaration.class, + n -> + n.getFullyQualifiedName().isPresent() + && n.getFullyQualifiedName().get().equals(erased)) + .orElse(null); + + return type; + } + + /** + * Given an AssociableToAST that could give a detached node, find its attached equivalent. This + * method is only necessary when you need to call resolve() or calculateResolvedType() on its + * children. Throws if the result is null; use {@link #tryFindAttachedNode(AssociableToAST, Map)} + * if you do not want this. + * + * @param associable The resolved definition that could yield a detached node + * @param fqnToCompilationUnits A map of fully-qualified type names to their compilation units + * @return The attached node + */ + public static Node findAttachedNode( + AssociableToAST associable, Map fqnToCompilationUnits) { + Node result = tryFindAttachedNode(associable, fqnToCompilationUnits); + + if (result == null) { + throw new RuntimeException("Could not find an attached AST node."); + } + + return result; + } + + /** + * Given an AssociableToAST that could give a detached node, find its attached equivalent. This + * method is only necessary when you need to call resolve() or calculateResolvedType() on its + * children. + * + * @param associable The resolved definition that could yield a detached node + * @param fqnToCompilationUnits A map of fully-qualified type names to their compilation units + * @return The attached node if found, or null if not found + */ + public static @Nullable Node tryFindAttachedNode( + AssociableToAST associable, Map fqnToCompilationUnits) { + Node detachedNode = associable.toAst().orElse(null); + + if (detachedNode == null) { + return null; + } + + TypeDeclaration declaration = getEnclosingClassLike(detachedNode); + + TypeDeclaration attached = + getTypeFromQualifiedName( + declaration.getFullyQualifiedName().orElse(""), fqnToCompilationUnits); + + if (attached == null) { + return null; + } + + return attached.findFirst(detachedNode.getClass(), n -> n.equals(detachedNode)).get(); + } + + /** + * Returns a type-compatible initializer for a field of the given type. + * + * @param variableType the type of the field + * @return a type-compatible initializer + */ + public static String getInitializerRHS(String variableType) { + return switch (variableType) { + case "byte" -> "(byte)0"; + case "short" -> "(short)0"; + case "int" -> "0"; + case "long" -> "0L"; + case "float" -> "0.0f"; + case "double" -> "0.0d"; + case "char" -> "'\\u0000'"; + case "boolean" -> "false"; + default -> "null"; + }; + } + + /** + * Get the enclosing anonymous class, if node is in one. If not, then this method returns null. + * + * @param node The node + * @return The enclosing anonymous class, or null if not found + */ + public static @Nullable ObjectCreationExpr getEnclosingAnonymousClassIfExists(Node node) { + Node parent = node.getParentNode().orElse(null); + Set parents = new HashSet<>(); + + while (parent != null) { + if (parent instanceof ObjectCreationExpr objectCreationExpr) { + if (objectCreationExpr.getAnonymousClassBody().isEmpty()) { + return null; + } + + for (BodyDeclaration anonymousBodyDecl : + objectCreationExpr.getAnonymousClassBody().get()) { + if (parents.contains(anonymousBodyDecl)) { + return objectCreationExpr; + } + } + + return null; + } + parents.add(parent); + parent = parent.getParentNode().orElse(null); + } + + return null; + } + + /** + * Given an expression that may be in an anonymous class, try to resolve it. If it is not in an + * anonymous class or cannot be resolved, return null. + * + * @param expression The expression to resolve + * @return The resolved value, or null if not found + */ + public static @Nullable Object tryResolveExpressionIfInAnonymousClass(Expression expression) { + ObjectCreationExpr anonymousClassDecl = getEnclosingAnonymousClassIfExists(expression); + + if (anonymousClassDecl == null) { + return null; + } + + // If in a callable declaration, add a temporary statement above the current + Statement current = null; + + Node node = anonymousClassDecl.getParentNode().orElse(null); + while (node != null) { + if (node instanceof Statement statement) { + current = statement; + break; + } + node = node.getParentNode().orElse(null); + } + + if (current == null) { + return null; + } + + // Temporarily insert a copy of the expression outside of the anonymous class and + // see if it is resolvable + Expression copy = expression.clone(); + copy.setParentNode(current.getParentNode().get()); + + if (copy instanceof Resolvable resolvable) { + try { + Object result = resolvable.resolve(); + copy.remove(); + return result; + } catch (RuntimeException e) { + // Go below and try to see if calculateResolvedType works + } + } + + try { + Object result = copy.calculateResolvedType(); + copy.remove(); + return result; + } catch (RuntimeException e) { + // A RuntimeException can also occur when we try to call calculateResolvedType. + // UnsolvedSymbolException is caught by RuntimeException. + } + + copy.remove(); + return null; + } + + /** + * Tries to find the corresponding declaration in an anonymous class. For example, a NameExpr + * would return its FieldDeclaration. This method is necessary since resolve() fails in an + * anonymous class with an unsolvable parent class, even if the member is defined within the + * anonymous class. Returns null if not found, or if the expression is not in an anonymous class. + * + * @param expr The expression + * @return The corresponding declaration, or null if not found + */ + public static @Nullable BodyDeclaration tryFindCorrespondingDeclarationInAnonymousClass( + Expression expr) { + ObjectCreationExpr anonymousClass = getEnclosingAnonymousClassIfExists(expr); + + if (anonymousClass == null) { + return null; + } + + // Check if the expression is within the anonymous class. This method is necessary because + // if the parent class of the anonymous class is not solvable, fields/methods defined within + // are also unsolvable + if (expr.isNameExpr() + || (expr.isFieldAccessExpr() && expr.asFieldAccessExpr().getScope().isThisExpr())) { + // Try to find the field + for (BodyDeclaration bodyDecl : anonymousClass.getAnonymousClassBody().get()) { + if (bodyDecl.isFieldDeclaration() + && bodyDecl.asFieldDeclaration().getVariables().stream() + .anyMatch(v -> v.getNameAsString().equals(expr.toString()))) { + return bodyDecl.asFieldDeclaration(); + } + } + } + // The current handling of methods in anonymous classes seems to work for now. If an issue + // arises in the future, add it here. + // TODO: add method finding based on name and parameter types + return null; + } + + /** + * Returns the resolved declaration of the expression if expression.resolve() throws an + * UnsupportedOperationException due to its qualifying expression being a constraint type. Returns + * null if any solving fails. + * + *

An example: resolving {@code foo.method()}, where {@code foo} is of type {@code ? extends + * T}. Resolving the method call will fail because T is a type parameter, not a declaration. This + * could occur in a lambda, where foo is a lambda parameter that is of type {@code ? extends T}, + * but this specific lambda has a type argument for T (like Foo). In this case, we'll want to find + * the corresponding method declaration from {@code Foo}. A similar case can be found in + * LambdaBodyStaticUnsolved2Test. + * + *

This method works under the assumption that the type argument type is solvable; if not, this + * will return null. + * + * @param expression The expression to find the declaration of (method, field) + * @return The resolved object, if it exists; null otherwise + */ + public static @Nullable Object tryFindCorrespondingDeclarationForConstraintQualifiedExpression( + Expression expression) { + if (!expression.hasScope()) { + return null; + } + + Expression scope = ((NodeWithTraversableScope) expression).traverseScope().get(); + ResolvedTypeParameterDeclaration bound; + + try { + ResolvedType resolvedType = scope.calculateResolvedType(); + if (resolvedType.isConstraint()) { + ResolvedLambdaConstraintType constraintType = resolvedType.asConstraintType(); + ResolvedType anyBound = constraintType.getBound(); + if (anyBound.isTypeVariable()) { + bound = anyBound.asTypeParameter(); + } else { + return anyBound; + } + } else { + return null; + } + } catch (UnsolvedSymbolException ex) { + return null; + } + + LambdaExpr parentLambda = null; + Node parent = expression.getParentNode().orElse(null); + + while (parent != null && parentLambda == null) { + if (parent instanceof LambdaExpr) { + parentLambda = (LambdaExpr) parent; + break; + } + parent = parent.getParentNode().orElse(null); + } + + if (parentLambda == null) { + return null; + } + + Node parentOfLambda = parentLambda.getParentNode().orElse(null); + + if (parentOfLambda instanceof MethodCallExpr methodCallParent) { + MethodUsage methodUsage; + ResolvedMethodDeclaration method; + try { + // Must use JavaParserFacade instead of .resolve() here, since we need to get the + // type parameters map: https://github.com/javaparser/javaparser/issues/2135 + JavaParserFacade parserFacade = JavaParserFacade.get(getTypeSolver()); + methodUsage = parserFacade.solveMethodAsUsage(methodCallParent); + method = methodUsage.getDeclaration(); + + // bound contains the type variable in the declaration of the parameter type, not the type + // variable in the method + + for (ResolvedType paramType : methodUsage.getParamTypes()) { + if (!paramType.isReferenceType()) { + continue; + } + + for (Pair typeParamMapPair : + paramType.asReferenceType().getTypeParametersMap()) { + // Can't trust bound.getQualifiedName(), because it gives the wrong qualified name + // if the parameter type declares a type parameter of the same name + if (typeParamMapPair.a.getName().equals(bound.getName())) { + if (typeParamMapPair.b.isTypeVariable()) { + // If the type variable is a type variable, we can use it directly + bound = typeParamMapPair.b.asTypeParameter(); + } else if (typeParamMapPair.b.isWildcard() + && typeParamMapPair.b.asWildcard().getBoundedType().isTypeVariable()) { + // If the type variable is a wildcard and is bounded, we can use it as well + bound = typeParamMapPair.b.asWildcard().getBoundedType().asTypeParameter(); + } + break; + } + } + } + } catch (UnsolvedSymbolException ex) { + return null; + } + + // Using what we already know, let's try to find the value of the implicit type argument + + // First, try to find a Type node that corresponds with the return type of methodCallParent. + // This could be a variable declarator, a return type in a method declaration, or a parameter + // type from another declaration. + + // Don't use a map here: in case a ResolvedType is the same for different parameters, we could + // have different pieces of information (i.e., two parameters are both resolved type T, but + // the args could be different in the method call) + List> resolvedTypeToPotentialASTTypes = new ArrayList<>(); + + if (methodCallParent.getParentNode().orElse(null) instanceof ReturnStmt returnStmt + && returnStmt.getParentNode().orElse(null) instanceof BlockStmt blockStmt + && blockStmt.getParentNode().orElse(null) instanceof MethodDeclaration methodDecl) { + try { + ResolvedType returnTypeResolved = methodDecl.getType().resolve(); + + resolvedTypeToPotentialASTTypes.add( + new Pair<>(method.getReturnType(), returnTypeResolved)); + } catch (UnsolvedSymbolException ex) { + // continue + } + } else if (methodCallParent.getParentNode().orElse(null) instanceof VariableDeclarator varDecl + && varDecl.getInitializer().isPresent() + && varDecl.getInitializer().get().equals(methodCallParent)) { + try { + ResolvedType typeResolved = varDecl.getType().resolve(); + + resolvedTypeToPotentialASTTypes.add(new Pair<>(method.getReturnType(), typeResolved)); + } catch (UnsolvedSymbolException ex) { + // continue + } + } else if (methodCallParent.getParentNode().orElse(null) instanceof AssignExpr assignExpr + && assignExpr.getValue().equals(methodCallParent)) { + try { + if (assignExpr.getTarget().isNameExpr()) { + resolvedTypeToPotentialASTTypes.add( + new Pair<>( + method.getReturnType(), + assignExpr.getTarget().asNameExpr().resolve().getType())); + } else if (assignExpr.getTarget().isFieldAccessExpr()) { + resolvedTypeToPotentialASTTypes.add( + new Pair<>( + method.getReturnType(), + assignExpr.getTarget().asFieldAccessExpr().resolve().getType())); + } + } catch (UnsolvedSymbolException ex) { + // continue + } + } else if (methodCallParent.getParentNode().orElse(null) instanceof MethodCallExpr methodCall + && methodCall.getArguments().contains(methodCallParent)) { + try { + ResolvedMethodDeclaration methodDecl = methodCall.resolve(); + + int argPos = methodCall.getArgumentPosition(methodCallParent); + + resolvedTypeToPotentialASTTypes.add( + new Pair<>(method.getReturnType(), methodDecl.getParam(argPos).getType())); + } catch (UnsolvedSymbolException ex) { + // continue + } + } + + for (int i = 0; i < methodCallParent.getArguments().size(); i++) { + Expression argument = methodCallParent.getArguments().get(i); + if (isExprTypeResolvable(argument)) { + ResolvedType argType = argument.calculateResolvedType(); + resolvedTypeToPotentialASTTypes.add(new Pair<>(method.getParam(i).getType(), argType)); + } + } + + Map typeVariableToTypesMap = new HashMap<>(); + for (Pair pair : resolvedTypeToPotentialASTTypes) { + if (pair.a instanceof ResolvedTypeParameterDeclaration typeVar) { + typeVariableToTypesMap.put(typeVar, pair.b); + } + + // If the parameter type is a reference type, then the argument type must also be one + // Importantly, the type variable names should be the same (but not the values) + + // An example pair might look like this: + // a: ReferenceType{java.lang.Iterable, + // typeParametersMap=TypeParametersMap{nameToValue={java.lang.Iterable.T=TypeVariable + // {JPTypeParameter(T, bounds=[])}}}} + // b: ReferenceType{java.lang.Iterable, + // typeParametersMap=TypeParametersMap{nameToValue={java.lang.Iterable.T=ReferenceType{com.example.sql.SqlParserPos, typeParametersMap=TypeParametersMap{nameToValue={}}}}}} + + // In this case, look at the type parameters map and see where the type variables can be + // mapped (i.e., JPTypeParameter(T) --> SqlParserPos) + if (pair.a instanceof ResolvedReferenceType refType) { + ResolvedReferenceType otherRefType = (ResolvedReferenceType) pair.b; + + for (Pair typeParamMapPair : + refType.getTypeParametersMap()) { + Pair other = + otherRefType.getTypeParametersMap().stream() + .filter(t -> t.a.equals(typeParamMapPair.a)) + .findFirst() + .orElse(null); + + if (other == null) { + continue; + } + + if (typeParamMapPair.b.isTypeVariable()) { + typeVariableToTypesMap.put(typeParamMapPair.b.asTypeParameter(), other.b); + } else if (typeParamMapPair.b.isWildcard() + && typeParamMapPair.b.asWildcard().getBoundedType().isTypeVariable() + && other.b.isWildcard()) { + typeVariableToTypesMap.put( + typeParamMapPair.b.asWildcard().getBoundedType().asTypeParameter(), + other.b.asWildcard().getBoundedType()); + } + } + } + } + + ResolvedType type = typeVariableToTypesMap.get(bound); + + if (type instanceof ResolvedReferenceType declaringType + && declaringType.getTypeDeclaration().isPresent()) { + if (expression.isMethodCallExpr()) + for (ResolvedMethodDeclaration potentialMethod : + declaringType.getTypeDeclaration().get().getDeclaredMethods()) { + if (!potentialMethod + .getName() + .equals(expression.asMethodCallExpr().getNameAsString())) { + continue; + } + + List<@Nullable ResolvedType> argumentTypes = + getArgumentTypesAsResolved(expression.asMethodCallExpr().getArguments()); + + if (argumentTypes.size() != potentialMethod.getNumberOfParams()) { + continue; + } + + boolean match = true; + for (int i = 0; i < argumentTypes.size(); i++) { + ResolvedType argType = argumentTypes.get(i); + ResolvedType paramType = potentialMethod.getParam(i).getType(); + + if (argType == null || !paramType.isAssignableBy(argType)) { + match = false; + break; + } + } + + if (match) { + return potentialMethod; + } + } + } + } + + return null; + } + + /** + * Given a list, return all subsets. + * + * @param The type of the list elements + * @param original The original list + * @return A list of all subsets + */ + public static List> generateSubsets(List original) { + List> subsets = new ArrayList<>(); + // There are 2^n - 1 subsets; each bit will determine if an element is included + int totalSubsets = 1 << original.size(); + + for (int i = 0; i < totalSubsets; i++) { + List subset = new ArrayList<>(); + for (int j = 0; j < original.size(); j++) { + if ((i & (1 << j)) != 0) { + subset.add(original.get(j)); + } + } + subsets.add(subset); + } + + return subsets; + } + + /** + * Given a list of collections, return all combinations of elements where one element is picked + * from each collection. For example, if you input a list [[1, 2], [3]], then output [[1, 3], [2, + * 3]]. + * + * @param The type of the list elements + * @param collections The list of collections to combine + * @return A list of all combinations of elements + */ + public static List> generateAllCombinations( + List> collections) { + List> combos = new ArrayList<>(); + combos.add(new ArrayList<>()); + + for (Collection set : collections) { + List> newCombos = new ArrayList<>(); + for (List combination : combos) { + for (T element : set) { + List newCombination = new ArrayList<>(combination); + newCombination.add(element); + newCombos.add(newCombination); + } + } + combos = newCombos; + } + + return combos; + } + + /** + * Same as {@link #generateAllCombinations(List)} but for Lists of Maps instead of Lists of + * Collections. Each map's entry set is used as the collection of elements to combine. + * + * @param The type of the keys in the maps + * @param The type of the values in the maps + * @param collections The list of maps to combine + * @return A list of all combinations of map entries + */ + public static List>> generateAllCombinationsForListOfMaps( + List> collections) { + // AbstractMap.Entry allows null values, unlike Map.Entry + List>> combinable = + collections.stream() + .map( + map -> + map.entrySet().stream() + .>map( + entry -> + (Map.Entry) + new AbstractMap.SimpleEntry<>(entry.getKey(), entry.getValue())) + .toList()) + .toList(); + return generateAllCombinations(combinable); + } + + /** + * Finds the closest method or lambda ancestor of a return statement. Will throw if neither is + * found, since a return statement is always located in one of these contexts. + * + * @param returnStmt The return statement + * @return The closest method or lambda ancestor (either of type MethodDeclaration or LambdaExpr) + */ + public static Node findClosestMethodOrLambdaAncestor(ReturnStmt returnStmt) { + @SuppressWarnings("unchecked") + Node ancestor = + returnStmt + .findAncestor( + n -> { + return n instanceof MethodDeclaration || n instanceof LambdaExpr; + }, + Node.class) + .get(); + return ancestor; + } + + /** + * This method handles a very specific case: if an unsolved method call has a scope whose type is + * a supertype of the type this method is being called in, then it will try to find a method with + * the same signature in the current type or a solvable supertype. + * + * @param methodCall The method call, unsolved + * @param fqnToCompilationUnit The map of FQNs to compilation units + * @return A method declaration if found, or null if not + */ + public static @Nullable MethodDeclaration tryFindMethodDeclarationWithSameSignatureFromThisType( + MethodCallExpr methodCall, Map fqnToCompilationUnit) { + if (!methodCall.hasScope()) { + return null; + } + + Expression scope = methodCall.getScope().get(); + + Type scopeType = tryGetTypeFromExpression(scope, fqnToCompilationUnit); + + if (scopeType == null) { + return null; + } + + TypeDeclaration enclosingClass = getEnclosingClassLike(methodCall); + + List> solvableAncestors = + getAllSolvableAncestors(enclosingClass, fqnToCompilationUnit); + + solvableAncestors.add(enclosingClass); + + boolean isScopeTypeAnAncestor = false; + for (TypeDeclaration thisOrAncestor : solvableAncestors) { + if (thisOrAncestor instanceof NodeWithExtends withExtends + && withExtends.getExtendedTypes().contains(scopeType)) { + isScopeTypeAnAncestor = true; + break; + } else if (thisOrAncestor instanceof NodeWithImplements withImplements + && withImplements.getImplementedTypes().contains(scopeType)) { + isScopeTypeAnAncestor = true; + break; + } + } + + if (!isScopeTypeAnAncestor) { + return null; + } + + MethodCallExpr clone = methodCall.clone(); + clone.removeScope(); + clone.setParentNode(methodCall.getParentNode().get()); + + MethodDeclaration method = + (MethodDeclaration) + tryFindSingleCallableForNodeWithUnresolvableArguments(clone, fqnToCompilationUnit); + + clone.remove(); + + if (method != null) { + return method; + } + + return null; + } + + /** + * Finds the least upper bound given a set of resolved types and solved member types. + * + * @param resolvedTypes The resolved types + * @param solvedMemberTypes The solved member types + * @return The least upper bound, or null if it is a primitive (this will only be the case if a + * primitive is inputted). Note that this is a ResolvedReferenceTypeDeclaration because this + * does not consider type variables. + */ + public static @Nullable ResolvedReferenceTypeDeclaration getLeastUpperBound( + List resolvedTypes, List solvedMemberTypes) { + if (resolvedTypes.isEmpty() && solvedMemberTypes.isEmpty()) { + throw new RuntimeException("No types available to compute least upper bound"); + } + + List combined = new ArrayList<>(); + + for (ResolvedType resolvedType : resolvedTypes) { + if (!resolvedType.isReferenceType()) { + // May be a type variable + continue; + } + + combined.add(resolvedType.asReferenceType().getTypeDeclaration().get()); + } + + for (SolvedMemberType solvedMemberType : solvedMemberTypes) { + String fqn = solvedMemberType.getFullyQualifiedNames().iterator().next(); + if (JavaLangUtils.isPrimitive(fqn)) { + return null; + } + + try { + combined.add(getTypeSolver().solveType(fqn)); + } catch (UnsolvedSymbolException e) { + // Type param, likely + } + } + + if (combined.isEmpty()) { + return null; + } + + Set intersectedAncestors = + Stream.concat( + combined.get(0).getAllAncestors().stream() + .map(anc -> anc.getTypeDeclaration().get()), + Stream.of(combined.get(0))) + .collect(Collectors.toCollection(HashSet::new)); + + for (ResolvedReferenceTypeDeclaration typeDecl : combined) { + intersectedAncestors.retainAll( + Stream.concat( + typeDecl.getAllAncestors().stream().map(anc -> anc.getTypeDeclaration().get()), + Stream.of(typeDecl)) + .toList()); + } + + return intersectedAncestors.stream() + .min( + (a, b) -> { + if (a.isAssignableBy(b)) return 1; + if (b.isAssignableBy(a)) return -1; + + // Prefer a class lub to an interface lub + if (a.isClass() && !b.isClass()) return -1; + if (!a.isClass() && b.isClass()) return 1; + + return 0; + }) + .get(); + } + + /** + * Given a method reference expression, find all possible methods it could be referring to. If the + * method reference scope type is unsolvable or no methods of matching name could be found (maybe + * in an unsolved ancestor), then this method returns an empty list. + * + * @param methodReference The method reference to find declarations of + * @return The list of matching resolved method declarations or an empty list if none could be + * found + */ + public static List getMethodDeclarationsFromMethodRef( + MethodReferenceExpr methodReference) { + if (!isExprTypeResolvable(methodReference.getScope())) { + return Collections.emptyList(); + } + + ResolvedType methodDeclaringType = methodReference.getScope().calculateResolvedType(); + + String methodName = methodReference.getIdentifier(); + + if (methodName.equals("new")) { + return methodDeclaringType.asReferenceType().getTypeDeclaration().get().getConstructors(); + } + + // Method references must be on reference types + return methodDeclaringType.asReferenceType().getAllMethods().stream() + .filter(method -> method.getName().equals(methodName)) + .toList(); + } + + /** + * Gets an import declaration based on a simple name, if one exists. Returns null if not found. + * + * @param name The simple name; does not contain a period + * @param cu The compilation unit + * @param mustBeStatic True if looking only for static imports + * @return The import declaration, if found; if not, then null + */ + public static @Nullable ImportDeclaration getImportDeclaration( + String name, CompilationUnit cu, boolean mustBeStatic) { + for (ImportDeclaration importDecl : cu.getImports()) { + if (mustBeStatic && !importDecl.isStatic()) { + continue; + } + + if (importDecl.getNameAsString().endsWith("." + name)) { + return importDecl; + } + } + + return null; + } + + /** + * Given a type declaration, return the resolved method declarations of all must implement + * methods. Must implement methods are methods that are abstract in JDK superclasses or + * non-default methods in JDK interfaces. + * + * @param typeDecl The type declaration + * @return The set of resolved JDK method declarations that must be implemented + */ + public static Set getAllMustImplementMethods( + TypeDeclaration typeDecl) { + Set methodsThatMustBeImplemented = new HashSet<>(); + + for (ResolvedReferenceTypeDeclaration jdkAncestor : getAllJDKAncestors(typeDecl)) { + for (ResolvedMethodDeclaration resolvedMethod : jdkAncestor.getDeclaredMethods()) { + // Skip methods that are already defined in java.lang.Object + if (JavaLangUtils.isJavaLangObjectMethod(resolvedMethod.getSignature())) { + continue; + } + + if (resolvedMethod.isAbstract()) { + methodsThatMustBeImplemented.add(resolvedMethod); + } + } + } + + return methodsThatMustBeImplemented; + } + + /** + * Given a type declaration, return the resolved method declarations of all must implement methods + * that do not have an existing declaration in the type or its solvable ancestors. Must implement + * methods are methods that are abstract in JDK superclasses or non-default methods. + * + * @param typeDecl The type declaration + * @param fqnToCompilationUnits A map of fully-qualified type names to their compilation units + * @return The set of resolved JDK method declarations that must be implemented and do not have an + * existing declaration + */ + public static Set getMustImplementMethodsWithNoExistingDeclaration( + TypeDeclaration typeDecl, Map fqnToCompilationUnits) { + Set methodsThatMustBeImplemented = + getAllMustImplementMethods(typeDecl); + + getAllMustImplementMethodsImpl(typeDecl, methodsThatMustBeImplemented, fqnToCompilationUnits); + + return methodsThatMustBeImplemented; + } + + /** + * Given a type declaration, return existing definitions of all must implement methods. Must + * implement methods are methods that are abstract in JDK superclasses or non-default methods in + * JDK interfaces. + * + * @param typeDecl The type declaration to which the parents belong + * @param nonJDKMustImplements The set of non-JDK must implement methods + * @param fqnToCompilationUnits A map of fully-qualified type names to their compilation units + * @return The list of existing method declarations that must be preserved + */ + public static List getDeclarationsForAllMustImplementMethods( + TypeDeclaration typeDecl, + Set nonJDKMustImplements, + Map fqnToCompilationUnits) { + Set methodsThatMustBeImplemented = + getAllMustImplementMethods(typeDecl); + + methodsThatMustBeImplemented.addAll(nonJDKMustImplements); + + return getAllMustImplementMethodsImpl( + typeDecl, methodsThatMustBeImplemented, fqnToCompilationUnits); + } + + /** + * Helper method for getAllMustImplementMethods. Given a set of JDK methods that must be + * implemented, find the closest method declaration to preserve (i.e., first check this class, + * then check its parent, then its grandparent, and so on.) + * + * @param typeDecl The type declaration + * @param methodsThatMustBeImplemented A set of methods that must be implemented + * @param fqnToCompilationUnits A map of fully-qualified type names to their compilation units + * @return The result list + */ + private static List getAllMustImplementMethodsImpl( + TypeDeclaration typeDecl, + Set methodsThatMustBeImplemented, + Map fqnToCompilationUnits) { + List result = new ArrayList<>(); + for (ResolvedMethodDeclaration resolvedMethodDecl : Set.copyOf(methodsThatMustBeImplemented)) { + List> typesInBetween = + getTypesInBetween(typeDecl, resolvedMethodDecl.declaringType()); + + Set exploredTypes = new HashSet<>(); + List>> typeParametersMaps = + new ArrayList<>(); + + MethodDeclaration earliestMethod = null; + int locationInPath = -1; + boolean hasJdkDefinition = false; + for (List path : typesInBetween) { + if (hasJdkDefinition) { + break; + } + + @MonotonicNonNull List> typeParametersMap = null; + for (int i = path.size(); i >= 0; i--) { + ResolvedReferenceTypeDeclaration declaration; + + if (i > 0) { + ResolvedReferenceType type = path.get(i - 1); + if (typeParametersMap == null) { + typeParametersMap = type.getTypeParametersMap(); + } else { + typeParametersMap = + composeTypeParameterMap(type.getTypeParametersMap(), typeParametersMap); + } + declaration = type.getTypeDeclaration().get(); + } else { + if (typeParametersMap == null) { + typeParametersMap = List.of(); + } + declaration = typeDecl.resolve(); + } + + exploredTypes.add(declaration); + + // Last in the path will be the type that contains resolvedMethodDecl + if (i == path.size()) { + continue; + } + + for (ResolvedMethodDeclaration resolvedMethod : declaration.getDeclaredMethods()) { + try { + if (resolvedMethod + .getSignature() + .equals( + getSignatureFromResolvedMethodWithTypeVariablesMap( + resolvedMethodDecl, typeParametersMap))) { + + MethodDeclaration methodDecl = + (MethodDeclaration) tryFindAttachedNode(resolvedMethod, fqnToCompilationUnits); + + if (methodDecl != null) { + if (!resolvedMethod.isAbstract()) { + if (i > locationInPath) { + earliestMethod = methodDecl; + locationInPath = i; + } + } + } else { + // We travel the path from the furthest ancestor to the closest ancestor, so if we + // find an abstract definition, set hasJdkDefinition to false since the abstract + // will override the concrete definition we may have found earlier in the path. + hasJdkDefinition = !resolvedMethod.isAbstract(); + } + + if (!resolvedMethod + .getQualifiedSignature() + .equals(resolvedMethodDecl.getQualifiedSignature())) { + methodsThatMustBeImplemented.remove(resolvedMethodDecl); + } + + break; + } + } catch (UnsolvedSymbolException ex) { + // It's possible that a method could reference an unsolved symbol; in this case, just + // skip it + } + } + } + + if (typeParametersMap == null) { + // This error is not possible (satisfy the null checker); the loop above always runs at + // least once. + throw new RuntimeException("Impossible"); + } + + typeParametersMaps.add(typeParametersMap); + } + + if (!hasJdkDefinition) { + // Maybe there is a method declaration for this, but it's not on the path from the + // current type declaration to the JDK type declaration. + // Look at types we haven't explored yet + + List allSolvableAncestors = new ArrayList<>(); + allSolvableAncestors.addAll(getAllJDKAncestors(typeDecl)); + allSolvableAncestors.addAll( + getAllSolvableAncestors(typeDecl, fqnToCompilationUnits).stream() + .map(anc -> anc.resolve()) + .toList()); + allSolvableAncestors.removeAll(exploredTypes); + allSolvableAncestors.remove(resolvedMethodDecl.declaringType()); + + for (ResolvedReferenceTypeDeclaration ancestor : allSolvableAncestors) { + if (hasJdkDefinition) { + break; + } + + List> pathsToAncestor = getTypesInBetween(typeDecl, ancestor); + // Get the type parameter maps from the declaring type of the method to the current type, + // then compose this to the path to the ancestor + for (List> potentialTypeParamMap : + typeParametersMaps) { + if (hasJdkDefinition) { + break; + } + List> typeParametersMap = + potentialTypeParamMap; + for (List path : pathsToAncestor) { + if (hasJdkDefinition) { + break; + } + + for (int i = path.size() - 1; i >= 0; i--) { + ResolvedReferenceType type = path.get(i); + typeParametersMap = + composeTypeParameterMap(type.getTypeParametersMap(), typeParametersMap); + + for (ResolvedMethodDeclaration resolvedMethod : + path.get(i).getTypeDeclaration().get().getDeclaredMethods()) { + if (resolvedMethod + .getSignature() + .equals( + getSignatureFromResolvedMethodWithTypeVariablesMap( + resolvedMethodDecl, typeParametersMap))) { + + MethodDeclaration methodDecl = + (MethodDeclaration) + tryFindAttachedNode(resolvedMethod, fqnToCompilationUnits); + + if (methodDecl != null) { + if (i > locationInPath) { + earliestMethod = methodDecl; + locationInPath = i; + } + } else { + // We travel the path from the furthest ancestor to the closest ancestor, so + // if we find an abstract definition, set hasJdkDefinition to false since + // the abstract will override the concrete definition we may have found + // earlier in the path. + hasJdkDefinition = !resolvedMethod.isAbstract(); + } + + if (resolvedMethod.isAbstract() + && !resolvedMethod + .getQualifiedSignature() + .equals(resolvedMethodDecl.getQualifiedSignature()) + && resolvedMethodDecl.declaringType().isAssignableBy(ancestor)) { + methodsThatMustBeImplemented.remove(resolvedMethodDecl); + } + + break; + } + } + } + } + } + } + } + + if (!hasJdkDefinition) { + if (earliestMethod != null) { + result.add(earliestMethod); + } + } + } + + return result; + } + + /** + * If from is A, and to is C, and A <: B and B <: C, then this should return a list of a list + * which contains B and C. Could contain multiple lists if there are multiple paths to the same + * type. + * + * @param from The first type + * @param to The last type + * @return The types in between the from and to types + */ + private static List> getTypesInBetween( + TypeDeclaration from, ResolvedReferenceTypeDeclaration to) { + List> result = new ArrayList<>(); + + List superTypes = getDirectSuperTypes(from); + + for (ClassOrInterfaceType superType : superTypes) { + try { + ResolvedReferenceType type = superType.resolve().asReferenceType(); + getTypesInBetweenImpl(type, to, new ArrayList<>(List.of(type)), result); + } catch (UnsolvedSymbolException e) { + // continue + } + } + + return result; + } + + /** + * Helper method for {@link #getTypesInBetween(TypeDeclaration, + * ResolvedReferenceTypeDeclaration)}. + * + * @param from The first type + * @param to The last type + * @param accumulator The accumulator for the current path + * @param result The result list + */ + private static void getTypesInBetweenImpl( + ResolvedReferenceType from, + ResolvedReferenceTypeDeclaration to, + List accumulator, + List> result) { + if (to.equals(from.getTypeDeclaration().orElse(null))) { + result.add(accumulator); + return; + } + + for (ResolvedReferenceType superType : from.getDirectAncestors()) { + List newAccumulator = new ArrayList<>(accumulator); + newAccumulator.add(superType); + getTypesInBetweenImpl(superType, to, newAccumulator, result); + } + } + + /** + * Composes a type parameter map from the previous and new type parameter maps. For example, if + * previousTypeParametersMap is T --> String and newTypeParametersMap is E --> T, then the + * composed map should be E --> String. + * + * @param previousTypeParametersMap The previous type parameter map + * @param newTypeParametersMap The new type parameter map + * @return The composed type parameter map + */ + private static List> composeTypeParameterMap( + List> previousTypeParametersMap, + List> newTypeParametersMap) { + List> result = new ArrayList<>(); + for (Pair entry : newTypeParametersMap) { + // In the above example, this would be T in E --> T + ResolvedType typeToReplace = entry.b; + + if (typeToReplace.isTypeVariable()) { + ResolvedTypeParameterDeclaration typeVar = typeToReplace.asTypeVariable().asTypeParameter(); + for (Pair pair : + previousTypeParametersMap) { + if (pair.a.equals(typeVar)) { + typeToReplace = pair.b; + break; + } + } + } + result.add(new Pair<>(entry.a, typeToReplace)); + } + + return result; + } + + /** + * Given a resolved method declaration and its declaring type's type variables map, return the + * method's signature with the type variables replaced by their resolved types. + * + *

For example, if the method is part of {@code Foo} and the type variables map is {@code T + * --> String}, then any parameters that match T will be replaced with String in the signature. + * + * @param method The resolved method declaration in the generic class + * @param typeVariablesMap The type variables map, which maps type variable declarations to their + * resolved types + * @return The method's signature with the type variables replaced by their resolved types + */ + public static String getSignatureFromResolvedMethodWithTypeVariablesMap( + ResolvedMethodDeclaration method, + List> typeVariablesMap) { + StringBuilder signature = new StringBuilder(method.getName() + "("); + + for (int i = 0; i < method.getNumberOfParams(); i++) { + ResolvedParameterDeclaration param = method.getParam(i); + + signature.append(getResolvedNameWithSubstitution(param.getType(), typeVariablesMap)); + + if (i < method.getNumberOfParams() - 1) { + signature.append(", "); + } + } + + signature.append(")"); + + return signature.toString(); + } + + /** + * Gets the resolved name of a type, substituting any type variables with their resolved types + * + * @param type The type to get the name of + * @param typeVariablesMap The type variables map + * @return The resolved name of the type, with type variables substituted + */ + private static String getResolvedNameWithSubstitution( + ResolvedType type, + List> typeVariablesMap) { + if (type.isTypeVariable()) { + for (Pair pair : typeVariablesMap) { + if (pair.a.equals(type.asTypeVariable().asTypeParameter())) { + return pair.b.describe(); + } + } + return type.asTypeVariable().describe(); + } + + if (type.isWildcard()) { + ResolvedWildcard wildcard = type.asWildcard(); + if (!wildcard.isBounded()) { + return "?"; + } + String bound = getResolvedNameWithSubstitution(wildcard.getBoundedType(), typeVariablesMap); + return wildcard.isExtends() ? "? extends " + bound : "? super " + bound; + } + + if (type.isReferenceType()) { + ResolvedReferenceType ref = type.asReferenceType(); + StringBuilder sb = new StringBuilder(ref.getQualifiedName()); + + List typeArgs = ref.typeParametersValues(); + if (!typeArgs.isEmpty()) { + sb.append("<"); + for (int i = 0; i < typeArgs.size(); i++) { + sb.append(getResolvedNameWithSubstitution(typeArgs.get(i), typeVariablesMap)); + if (i < typeArgs.size() - 1) sb.append(", "); + } + sb.append(">"); + } + + return sb.toString(); + } + + if (type.isArray()) { + return getResolvedNameWithSubstitution( + type.asArrayType().getComponentType(), typeVariablesMap) + + "[]"; + } + + return type.describe(); + } + + /** + * Returns true if the given type or any of its outer types are private. + * + * @param fqn The FQN of the type to check + * @param fqnToCompilationUnits A map of FQNs to compilation units + * @return True if the type or any of its outer types are private, false otherwise + */ + public static boolean areTypeOrOuterTypesPrivate( + String fqn, Map fqnToCompilationUnits) { + TypeDeclaration typeDecl = getTypeFromQualifiedName(fqn, fqnToCompilationUnits); + if (typeDecl != null) { + if (typeDecl.isPrivate()) { + return true; + } + + int lastIndexOfDot = fqn.lastIndexOf('.'); + if (lastIndexOfDot == -1) { + return false; + } + + TypeDeclaration outerType = getEnclosingClassLikeOptional(typeDecl); + + while (outerType != null) { + // A non-public inner class is private. + if (!typeDecl.isPublic()) { + return true; + } + + typeDecl = outerType; + outerType = getEnclosingClassLikeOptional(outerType); + + return true; + } + } + return false; + } } diff --git a/src/main/java/org/checkerframework/specimin/JavaTypeCorrect.java b/src/main/java/org/checkerframework/specimin/JavaTypeCorrect.java deleted file mode 100644 index d09847651..000000000 --- a/src/main/java/org/checkerframework/specimin/JavaTypeCorrect.java +++ /dev/null @@ -1,647 +0,0 @@ -package org.checkerframework.specimin; - -import com.google.common.base.Splitter; -import java.io.BufferedReader; -import java.io.File; -import java.io.IOException; -import java.io.InputStreamReader; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.Arrays; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Set; -import org.checkerframework.checker.nullness.qual.Nullable; - -/** - * This class uses javac to analyze files. If there are any incompatible type errors in those files, - * this class can suggest changes to be made to the existing types in the input files to solve all - * the type errors. Note: This class can only solve type errors where there are mismatch between - * types. For cases where there is type inference failed or unfound symbol, try to use - * UnsolvedSymbolVisitor to add the missing files to the input first. {@link UnsolvedSymbolVisitor} - */ -class JavaTypeCorrect { - - /** List of the files to correct the types */ - public Set fileNameList; - - /** - * The root directory of the files in the fileNameList. If there are more than one root - * directories involved, consider using more than one instances of JavaTypeCorrect - */ - public String sourcePath; - - /** - * This map is for type correcting. The key is the name of the current incorrect type, and the - * value is the name of the desired correct type. - */ - private final Map typeToChange; - - /** - * A map that associates the file directory with the set of fully qualified names of types used - * within that file. - */ - private Map> fileAndAssociatedTypes = new HashMap<>(); - - /** - * Synthetic types that need to extend or implement a class/interface. Note that the stored - * Strings are simple names (because javac's error messages only give simple names), which is safe - * because the worst thing that might happen is that an extra synthetic class might accidentally - * extend or implement an unnecessary interface. - */ - private final Map extendedTypes = new HashMap<>(); - - /** - * This map associates the name of a class with the name of the unresolved interface due to - * missing method implementations. - */ - private final Map classAndUnresolvedInterface = new HashMap<>(); - - /** - * This map associates a method reference usage to a map of argument corrections, containing the - * method reference and the correct number of arguments - */ - private final Map methodRefToCorrectParameters = new HashMap<>(); - - /** This map associates a method reference usage to whether its return type is void or not. */ - private final Map methodRefVoidness = new HashMap<>(); - - /** The name used for a synthetic, unconstrained type variable. */ - public static final String SYNTHETIC_UNCONSTRAINED_TYPE = "SyntheticUnconstrainedType"; - - /** - * Create a new JavaTypeCorrect instance. The directories of files in fileNameList are relative to - * rootDirectory, and rootDirectory is an absolute path - * - * @param rootDirectory the root directory of the files to correct types - * @param fileNameList the list of the relative directory of the files to correct types - */ - public JavaTypeCorrect( - String rootDirectory, - Set fileNameList, - Map> fileAndAssociatedTypes) { - this.fileNameList = fileNameList; - this.sourcePath = new File(rootDirectory).getAbsolutePath(); - this.typeToChange = new HashMap<>(); - this.fileAndAssociatedTypes = fileAndAssociatedTypes; - } - - /** - * Get the value of typeToChange - * - * @return the value of typeToChange - */ - public Map getTypeToChange() { - return typeToChange; - } - - /** - * Get the value of classAndUnresolvedInterface. - * - * @return the value of classAndUnresolvedInterface. - */ - public Map getClassAndUnresolvedInterface() { - return classAndUnresolvedInterface; - } - - /** - * Get the value of methodRefToCorrectParameters. - * - * @return the value of methodRefToCorrectParameters. - */ - public Map getMethodRefToCorrectParameters() { - return methodRefToCorrectParameters; - } - - /** - * Get the value of methodRefVoidness. - * - * @return the value of methodRefVoidness. - */ - public Map getMethodRefVoidness() { - return methodRefVoidness; - } - - /** - * Get the simple names of synthetic classes that should extend or implement a class/interface (it - * is not known at this point which). Both keys and values are simple names, due to javac - * limitations. - * - * @return the map described above. - */ - public Map getExtendedTypes() { - // Before returning, purge any entries that are obviously bad according to - // the following simple heuristic(s): - // * don't extend known-final classes from the JDK, like java.lang.String. - // * don't add change types to "SyntheticUnconstrainedType" - Set toRemove = new HashSet<>(0); - for (Map.Entry entry : extendedTypes.entrySet()) { - if (JavaLangUtils.isFinalJdkClass(entry.getValue())) { - toRemove.add(entry.getKey()); - } - // Don't let errors related sythetic unconstrained types added by Specimin propagate. - if (entry.getValue().equals(SYNTHETIC_UNCONSTRAINED_TYPE)) { - toRemove.add(entry.getKey()); - } - } - for (String s : toRemove) { - extendedTypes.remove(s); - } - return extendedTypes; - } - - /** - * This method updates typeToChange by using javac to run all the files in fileNameList and - * analyzing the error messages returned by javac - */ - public void correctTypesForAllFiles() { - for (String fileName : fileNameList) { - runJavacAndUpdateTypes(fileName); - } - } - - /** - * This method uses javac to run a file and updates typeToChange if that file has any incompatible - * type error - * - * @param filePath the directory of the file to be analyzed - */ - public void runJavacAndUpdateTypes(String filePath) { - Path outputDir; - try { - outputDir = Files.createTempDirectory("specimin-javatypecorrect"); - } catch (IOException e) { - throw new RuntimeException("failed to create a temporary directory"); - } - - try { - String command = "javac"; - // Note: -d to a tempdir is used to avoid generating .class files amongst the user's files - // when compilation succeeds. -Xmaxerrs 0 is used to print out all error messages. - String[] arguments = { - command, - "-d", - outputDir.toAbsolutePath().toString(), - "-sourcepath", - sourcePath, - sourcePath + "/" + filePath, - "-Xmaxerrs", - "0" - }; - ProcessBuilder processBuilder = new ProcessBuilder(arguments); - processBuilder.redirectErrorStream(true); - Process process = processBuilder.start(); - BufferedReader reader = - new BufferedReader( - new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8)); - String line; - - // These temporaries are necessary to handle various multi-line error messages. - // We support multiline error messages of the following kinds: - // * incompatible equality constraints - // * bad operand types for binary operators - // * for-each not applicable to expression type - // * incompatiable method reference types - - // These are temporaries for the equality constraints case. - String[] firstConstraints = {"equality constraints: "}; - String[] secondConstraints = {"lower bounds: "}; - String firstConstraintType = ""; - - // These are temporaries for the binary operator case. - String binOp = null; - String firstBinOpType = null; - - // These temporaries are for the for-each case. - String loopType = null; - boolean lookingForLoopType = false; - - // These temporaries are for the invalid method reference cases. - boolean lookingForInvalidMethodReference = false; - String methodReferenceUsage = null; - - StringBuilder lines = new StringBuilder("\n"); - - lines: - while ((line = reader.readLine()) != null) { - lines.append(line); - // Note: this is before PrunerVisitor's phase, meaning that these methods are never in the - // source codes to begin with. This usually happens when a file is isolated from its - // package, and its parent is supposed to override some of the methods in the given - // interface. For these cases, if the interface is not from Java language, we will modify - // the codes of the interface. Otherwise, we will remove that interface completely. - - // TODO: Update Specimin to generate a synthetic version for the missing parent class with - // synthetic method implementations, particularly if the targeted method invokes a method - // from the parent class that implements a method from a Java language interface. - if (line.contains("not abstract and does not override abstract method")) { - updateClassAndUnresolvedInterface(line); - } - - // For-each logic - if (loopType != null) { - // intermediate parts of the error message, which we can skip - if (line.contains("^")) { - continue; - } - if (line.contains("required: array or java.lang.Iterable")) { - continue; - } - // the next line should look like: "found: GetFoosReturnType" - if (!line.contains("found: ")) { - throw new RuntimeException( - "could not complete a for-each correction, because encountered " - + "an unexpected line in a javac error message: " - + line); - } - String typeToCorrect = line.substring(line.indexOf(':') + 1).trim(); - changeType(typeToCorrect, loopType + "[]"); - loopType = null; - continue; - } - if (lookingForLoopType) { - // line should look like: "for (Foo f : b.getFoos()) {"; we want to extract the "Foo" - // and put it into loopType. - if (loopType != null) { - throw new RuntimeException( - "loopType wasn't null when trying to set a loopType: " + loopType); - } - int startIndex = line.indexOf('(') + 1; - loopType = line.substring(startIndex, line.indexOf(' ', startIndex)); - lookingForLoopType = false; - continue; - } - if (line.contains("for-each not applicable to expression type")) { - lookingForLoopType = true; - continue; - } - - if (lookingForInvalidMethodReference) { - if (line.contains("::")) { - methodReferenceUsage = line; - } else if (line.contains("^")) { - if (methodReferenceUsage == null) { - throw new RuntimeException("Method reference not found"); - } - - // This is the start of the method reference; travel forwards until we hit a non- - // alphanumeric character, except for : - int start = line.indexOf("^"); - - int end = start; - while (end < methodReferenceUsage.length() - && (Character.isLetterOrDigit(methodReferenceUsage.charAt(end)) - || methodReferenceUsage.charAt(end) == ':')) { - end++; - } - - methodReferenceUsage = methodReferenceUsage.substring(start, end); - } - // method x in class y cannot be applied to given types - // then, it gives you a line with required: and all the necessary parameters - else if (line.contains("required:")) { - if (methodReferenceUsage == null) { - throw new RuntimeException("Method reference not found"); - } - if (line.contains("no arguments")) { - methodRefToCorrectParameters.put(methodReferenceUsage, ""); - } else { - methodRefToCorrectParameters.put( - methodReferenceUsage, line.trim().substring("required:".length()).trim()); - } - - lookingForInvalidMethodReference = false; - methodReferenceUsage = null; - continue; - } - // handle method return type (this is mutually exclusive with - // argument types; if argument types are not valid, this error message - // will not show up) - else if (line.contains("void cannot be converted to")) { - if (methodReferenceUsage == null) { - throw new RuntimeException("Method reference not found"); - } - methodRefVoidness.put(methodReferenceUsage, true); - lookingForInvalidMethodReference = false; - methodReferenceUsage = null; - continue; - } - } - - if (line.contains("error: incompatible types") - || line.contains("error: incomparable types")) { - if (line.contains("invalid method reference") - || line.contains("bad return type in method reference")) { - lookingForInvalidMethodReference = true; - continue; - } else { - updateTypeToChange(line, filePath); - } - continue lines; - } - if (line.contains("is not compatible with")) { - updateTypeToChange(line, filePath); - continue lines; - } - if (line.contains("bad operand types for binary operator")) { - if (binOp != null || firstBinOpType != null) { - throw new RuntimeException("failed to complete a binary operator correction: " + lines); - } - // the form of the error is "bad operand types for binary operator '||'" - binOp = line.substring(line.indexOf('\'') + 1, line.lastIndexOf('\'')); - continue lines; - } - if (binOp != null && line.contains("first type: ")) { - if (firstBinOpType != null) { - throw new RuntimeException("failed to complete a binary operator correction: " + lines); - } - firstBinOpType = line.replace("first type:", "").trim(); - continue lines; - } - if (binOp != null && firstBinOpType != null && line.contains("second type: ")) { - String secondBinOpType = line.replace("second type:", "").trim(); - updateTypesForBinaryOperator(binOp, firstBinOpType, secondBinOpType); - binOp = null; - firstBinOpType = null; - continue lines; - } - // these type error with constraint types will be in a pair of lines - for (String firstConstraint : firstConstraints) { - if (line.contains(firstConstraint)) { - firstConstraintType = line.replace(firstConstraint, "").trim(); - continue lines; - } - } - for (String secondConstraint : secondConstraints) { - if (line.contains(secondConstraint)) { - String secondConstraintType = line.replace(secondConstraint, "").trim(); - // These "constraint types" may include more than one type, especially if - // they are equality constraints. The strategy for solving them below is - // quite coarse, but it works on most examples. TODO: do this properly by - // reasoning about what the constraints mean. - Set constraints = new HashSet<>(2); - constraints.addAll(List.of(firstConstraintType.split(","))); - constraints.addAll(List.of(secondConstraintType.split(","))); - if (constraints.size() == 2) { - String[] constraintsArray = constraints.toArray(new String[0]); - firstConstraintType = constraintsArray[0]; - secondConstraintType = constraintsArray[1]; - if (isSynthetic(firstConstraintType)) { - changeType(firstConstraintType, secondConstraintType); - } else if (isSynthetic(secondConstraintType)) { - changeType(secondConstraintType, firstConstraintType); - } else { - // We used to throw an exception here. However, sometimes - // this case does happen while reducing large projects - we saw - // it while reducing e.g. Apache Cassandra. It may still indicate - // a problem when we encounter it, but I'm not sure that it is: - // this may happen sometimes during intermediate stages of Specimin. - } - } else { - // do nothing - we can't solve this case. - // TODO: properly solve sets of three or more constraints - } - - firstConstraintType = ""; - continue lines; - } - } - } - } catch (IOException e) { - // TODO: Handle this properly - System.out.println(e); - } - } - - /** - * Updates the two input types (if they are synthetic) to match the requirements of the given - * binary operator. - * - * @param binOp a string representation of a binary operator, such as "||" - * @param firstBinOpType the first possibly-not-matching type - * @param secondBinOpType the second possibly-not-matching type - */ - private void updateTypesForBinaryOperator( - String binOp, String firstBinOpType, String secondBinOpType) { - List requiredTypes = Arrays.asList(JavaLangUtils.getTypesForOp(binOp)); - if (requiredTypes.contains(firstBinOpType)) { - changeType(secondBinOpType, firstBinOpType); - } else if (requiredTypes.contains(secondBinOpType)) { - changeType(firstBinOpType, secondBinOpType); - } else { - assert !"==".equals(binOp) && !"!=".equals(binOp); - changeType(firstBinOpType, requiredTypes.get(0)); - changeType(secondBinOpType, requiredTypes.get(0)); - } - } - - /** - * Parses a type from a space-separated error message. - * - * @param splitErrorMessage the space-separated error message - * @param startIndex the index into the error message at which the type starts - * @param next the stop word to look for. Null if the type should go to the end of the input list. - * @return the type as a string - */ - private String getTypeFrom( - List splitErrorMessage, int startIndex, @Nullable String next) { - StringBuilder result = new StringBuilder(); - int i = startIndex; - while (i < splitErrorMessage.size() && !Objects.equals(splitErrorMessage.get(i), next)) { - if (!splitErrorMessage.get(i).startsWith("@")) { - result.append(" ").append(splitErrorMessage.get(i)); - } - i++; - } - return result.toString().trim(); - } - - /** - * This method updates typeToChange by relying on the error messages from javac - * - * @param errorMessage the error message to be analyzed - * @param filePath the path of the file where this error happens - */ - private void updateTypeToChange(String errorMessage, String filePath) { - // TODO: splitting on spaces here isn't safe, because types can contain spaces (e.g., if they - // are wildcards or have multiple type parameters!). We should find an alternative way to parse - // these error messages that doesn't require us to then re-parse the types from this list. - List splitErrorMessage = Splitter.onPattern("\\s+").splitToList(errorMessage); - if (splitErrorMessage.size() < 7) { - throw new RuntimeException("Unexpected type error messages: " + errorMessage); - } - /* There are four possible forms of error messages in total: - * 1. error: incompatible types: cannot be converted to - */ - if (errorMessage.contains("cannot be converted to")) { - String rhs = getTypeFrom(splitErrorMessage, 4, "cannot"); - int toIndex = splitErrorMessage.indexOf("to"); - String lhs = getTypeFrom(splitErrorMessage, toIndex + 1, null); - if ("Throwable".equals(lhs)) { - // Since all the checked exceptions have already been handled by UnsolvedSymbolVisitor, we - // know that all the remaining uncompiled exceptions are unchecked. - extendedTypes.put(rhs, "RuntimeException"); - } else if (isSynthetic(lhs)) { - // This situation occurs if we have created a synthetic field - // (e.g., in a superclass) that has a type that doesn't match the - // type of the RHS. In this case, the "correct" type is wrong, and - // the "incorrect" type is the actual type of the RHS. - changeType(lhs, tryResolveFullyQualifiedType(rhs, filePath)); - } else if (isSynthetic(rhs)) { - changeType(rhs, tryResolveFullyQualifiedType(lhs, filePath)); - } else { - // In this case, neither is truly synthetic (both must be used - // in the target), so make the rhs a subtype of the lhs. - // TODO: we must check here that there is no entry for the rhs already. - // However, it's not clear what the right behavior is when there is - // an existing entry. I've set this up to do nothing to avoid thrashing - // behavior like that seen in https://github.com/njit-jerse/specimin/issues/279. - // However, this does sometimes occur, including in some of our test targets, - // so we have to not crash. - if (!extendedTypes.containsKey(rhs)) { - extendedTypes.put(rhs, lhs); - } - } - } - /* - * 2. error: incomparable types: Type1 and Type2 - * 3. return type is not compatible with (triggered when there is type mismatching in inheritance) - * 4. error: incompatible types: found required (unknown triggers) - */ - else { - String rhs, lhs; - if (errorMessage.contains("incomparable types")) { - // Case 2 - rhs = getTypeFrom(splitErrorMessage, 4, "and"); - lhs = getTypeFrom(splitErrorMessage, splitErrorMessage.indexOf("and") + 1, null); - } else if (errorMessage.contains("is not compatible with")) { - // Case 3 - rhs = getTypeFrom(splitErrorMessage, 3, "is"); - lhs = getTypeFrom(splitErrorMessage, splitErrorMessage.indexOf("with") + 1, null); - } else { - // Case 4 - rhs = getTypeFrom(splitErrorMessage, 5, "required"); - lhs = getTypeFrom(splitErrorMessage, splitErrorMessage.indexOf("required") + 1, null); - } - if (isSynthetic(lhs)) { - changeType(lhs, tryResolveFullyQualifiedType(rhs, filePath)); - } else if (isSynthetic(rhs)) { - changeType(rhs, tryResolveFullyQualifiedType(lhs, filePath)); - } else { - extendedTypes.put(rhs, lhs); - } - } - } - - /** - * All instances of the synthetic "incorrect type" will be replaced with the "correct type" in the - * output of Specimin. This method does handle cases where at least two different types need to be - * matched (i.e., upper bounds), and should always be called rather than updating {@link - * #typeToChange} directly. - * - * @param incorrectType an incorrect synthetic type that is causing a type error - * @param correctType a correct type that the incorrect type must be a supertype of, based on the - * output of javac - */ - private void changeType(String incorrectType, String correctType) { - if (typeToChange.containsKey(incorrectType)) { - String otherCorrectType = typeToChange.get(incorrectType); - if (!otherCorrectType.equals(correctType)) { - boolean isSyntheticReturnType = incorrectType.endsWith("ReturnType"); - if (!isSyntheticReturnType) { - // we require a LUB: don't do a direct conversion between the types, but - // instead retain the "incorrect" synthetic type as a mutual top type - // for the two other "correct" types. - typeToChange.remove(incorrectType); - // TODO: what if one of these "correct" types is non-synthetic? - // Is that possible? What would the consequences be if so? - extendedTypes.put(correctType, incorrectType); - extendedTypes.put(otherCorrectType, incorrectType); - // once we've made this lub correction, we don't want to - // continue with our main fix strategy - return; - } else { - // we require a GLB: that is, this synthetic return type needs to be _used_ in - // two different contexts: one where correctType is required, and another - // where otherCorrectType is required. Instead of worrying about making a correct GLB, - // instead just use an unconstrained type variable. - typeToChange.put( - incorrectType, - "<" + SYNTHETIC_UNCONSTRAINED_TYPE + "> " + SYNTHETIC_UNCONSTRAINED_TYPE); - return; - } - } - } - - typeToChange.put(incorrectType, correctType); - } - - /** - * This method updates the map of classes and their unresolved interfaces based on an error - * message from javac. - * - * @param line an error message from javac. - */ - private void updateClassAndUnresolvedInterface(String line) { - List splitErrorMessage = Splitter.onPattern("\\s+").splitToList(line); - // such an error message will have this format: - // error: is not abstract and does not override abstract method in - // - if (splitErrorMessage.size() < 3) { - // technically it is more than 3, but this is all we need to avoid false warnings. - throw new RuntimeException("Unexpected type error messages: " + line); - } - String className = splitErrorMessage.get(2); - String interfaceName = splitErrorMessage.get(splitErrorMessage.size() - 1); - classAndUnresolvedInterface.put(className, interfaceName); - } - - /** - * This method tries to get the fully-qualified name of a type based on the simple name of that - * type and the class file where that type is used. - * - * @param type the type to be taken as input - * @param filePath the path of the file where type is used - * @return the fully-qualified name of that type if any. Otherwise, return the original expression - * of type. - */ - public String tryResolveFullyQualifiedType(String type, String filePath) { - // type is already in the fully qualified format - if (Splitter.onPattern("\\.").splitToList(type).size() > 1) { - return type; - } - String typeVariable = ""; - if (type.contains("<")) { - typeVariable = type.substring(type.indexOf("<")); - type = type.substring(0, type.indexOf("<")); - } - if (fileAndAssociatedTypes.containsKey(filePath)) { - Set fullyQualifiedType = fileAndAssociatedTypes.get(filePath); - for (String typeFullName : fullyQualifiedType) { - if (typeFullName.substring(typeFullName.lastIndexOf(".") + 1).equals(type)) { - return typeFullName + typeVariable; - } - } - } - return type + typeVariable; - } - - /** - * returns true iff the given simple type's name matches one of the patterns used by - * UnsolvedSymbolVisitor when creating synthetic classes - * - * @param typename a simple type name - * @return true if the name can be synthetic - */ - public static boolean isSynthetic(String typename) { - return typename.startsWith("SyntheticTypeFor") - || typename.endsWith("ReturnType") - || typename.startsWith("SyntheticFunction") - || typename.startsWith("SyntheticConsumer") - || typename.endsWith("SyntheticType"); - } -} diff --git a/src/main/java/org/checkerframework/specimin/MustImplementMethodsVisitor.java b/src/main/java/org/checkerframework/specimin/MustImplementMethodsVisitor.java deleted file mode 100644 index 98ffad51a..000000000 --- a/src/main/java/org/checkerframework/specimin/MustImplementMethodsVisitor.java +++ /dev/null @@ -1,407 +0,0 @@ -package org.checkerframework.specimin; - -import com.github.javaparser.ast.Node; -import com.github.javaparser.ast.NodeList; -import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration; -import com.github.javaparser.ast.body.EnumDeclaration; -import com.github.javaparser.ast.body.MethodDeclaration; -import com.github.javaparser.ast.body.Parameter; -import com.github.javaparser.ast.expr.MethodCallExpr; -import com.github.javaparser.ast.expr.SuperExpr; -import com.github.javaparser.ast.stmt.BlockStmt; -import com.github.javaparser.ast.type.ClassOrInterfaceType; -import com.github.javaparser.ast.type.ReferenceType; -import com.github.javaparser.ast.visitor.Visitable; -import com.github.javaparser.resolution.UnsolvedSymbolException; -import com.github.javaparser.resolution.declarations.ResolvedMethodDeclaration; -import com.github.javaparser.resolution.declarations.ResolvedParameterDeclaration; -import com.github.javaparser.resolution.types.ResolvedReferenceType; -import com.github.javaparser.resolution.types.ResolvedType; -import com.github.javaparser.resolution.types.parametrization.ResolvedTypeParametersMap; -import java.util.Collection; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.regex.Pattern; -import org.checkerframework.checker.nullness.qual.Nullable; - -/** - * If a used class includes methods that must be implemented (because it extends an abstract class - * or implements an interface that requires them), this visitor marks them for preservation. Should - * run after the list of used classes is finalized. - */ -public class MustImplementMethodsVisitor extends SpeciminStateVisitor { - /** - * Constructs a new SolveMethodOverridingVisitor with the provided sets of target methods, used - * members, and used classes. - * - * @param previousVisitor the last visitor to run - */ - public MustImplementMethodsVisitor(SpeciminStateVisitor previousVisitor) { - super(previousVisitor); - } - - @Override - @SuppressWarnings("nullness:return") // ok to return null, because this is a void visitor - public Visitable visit(ClassOrInterfaceDeclaration type, Void p) { - if (type.getFullyQualifiedName().isPresent() - && usedTypeElements.contains(type.getFullyQualifiedName().get())) { - return super.visit(type, p); - } else { - // the effect of not calling super here is that only used classes - // will actually be visited by this class - return null; - } - } - - @Override - public Visitable visit(MethodDeclaration method, Void p) { - ResolvedMethodDeclaration overridden = getOverriddenMethodInSuperClass(method); - // two cases: the method is a solvable override that will be preserved, and we can show that - // it is abstract (before the ||), or we can't solve the override but there - // is an @Override annotation. This relies on the use of @Override when - // implementing required methods from interfaces in the target code, - // but unfortunately I think it's the best that we can do here. (@Override - // is technically optional, but it is widely used.) - // However, if the current method is in a target type and in an interface/abstract class, - // we don't need to preserve the method since there are no children that require its definition - if (isPreservedAndAbstract(overridden) - || (overridden == null - && overridesAnInterfaceMethod(method) - && !isParentTargetAndInterfaceOrAbstract(method))) { - ResolvedMethodDeclaration resolvedMethod = method.resolve(); - Map returnAndParamAndThrowTypes = new HashMap<>(); - try { - returnAndParamAndThrowTypes.put( - resolvedMethod.getReturnType().describe(), resolvedMethod.getReturnType()); - for (int i = 0; i < resolvedMethod.getNumberOfParams(); ++i) { - ResolvedParameterDeclaration param = resolvedMethod.getParam(i); - returnAndParamAndThrowTypes.put(param.describeType(), param.getType()); - } - for (ReferenceType thrownException : method.getThrownExceptions()) { - ResolvedType resolvedException = thrownException.resolve(); - returnAndParamAndThrowTypes.put(resolvedException.describe(), resolvedException); - } - } catch (UnsolvedSymbolException | UnsupportedOperationException e) { - // In this case, don't keep the method (it won't compile anyway, - // since some needed symbol isn't available). TODO: find a way to trigger the - // creation of a synthetic class for the unsolved symbol at this point. - return super.visit(method, p); - } - usedMembers.add(resolvedMethod.getQualifiedSignature()); - for (String type : returnAndParamAndThrowTypes.keySet()) { - String originalType = type; - type = type.trim(); - if (type.contains("<")) { - // remove generics, if present, since this type will be used in - // an import - type = type.substring(0, type.indexOf("<")); - } - // also remove array types - if (type.contains("[]")) { - type = type.replace("[]", ""); - } - - boolean previouslyIncluded = usedTypeElements.contains(type); - - usedTypeElements.add(type); - - ResolvedType resolvedType = returnAndParamAndThrowTypes.get(originalType); - - if (!previouslyIncluded && resolvedType != null && resolvedType.isReferenceType()) { - addAllResolvableAncestors(resolvedType.asReferenceType()); - } - } - } - return super.visit(method, p); - } - - /** - * Returns true if the given method is abstract. - * - * @param method a possibly-null method declaration - * @return true iff the input is non-null and abstract - */ - private boolean isPreservedAndAbstract(@Nullable ResolvedMethodDeclaration method) { - if (method == null || !method.isAbstract()) { - return false; - } - String methodSignature = method.getQualifiedSignature(); - // These classes are beyond our control. It's better to retain the implementations of all - // abstract methods to ensure the code remains compilable. - if (JavaLangUtils.inJdkPackage(methodSignature)) { - return true; - } - return usedMembers.contains(methodSignature); - } - - /** - * Returns true iff the parent is an abstract class/interface and if it is a targeted type. - * - * @param node the Node to check - * @return true iff the parent is a target abstract class/interface - */ - private boolean isParentTargetAndInterfaceOrAbstract(Node node) { - Node parent = JavaParserUtil.getEnclosingClassLike(node); - - if (parent instanceof ClassOrInterfaceDeclaration - && (((ClassOrInterfaceDeclaration) parent).isInterface() - || ((ClassOrInterfaceDeclaration) parent).isAbstract())) { - String enclosingClassName = - ((ClassOrInterfaceDeclaration) parent).getFullyQualifiedName().orElse(null); - - if (enclosingClassName != null) { - for (String targetMethod : targetMethods) { - if (targetMethod.startsWith(enclosingClassName)) { - return true; - } - } - for (String targetField : targetFields) { - if (targetField.startsWith(enclosingClassName)) { - return true; - } - } - } - } - return false; - } - - /** - * Returns true iff the given method declaration is overriding a preserved unimplemented method in - * an implemented interface. This is an expensive check that searches the implemented interfaces. - * - * @param method the method declaration to check - * @return true iff the given method definitely overrides a preserved method in an interface - */ - private boolean overridesAnInterfaceMethod(MethodDeclaration method) { - ResolvedMethodDeclaration resolved; - String signature; - try { - resolved = method.resolve(); - signature = resolved.getSignature(); - } catch (UnsolvedSymbolException | UnsupportedOperationException e) { - // Some part of the signature isn't being preserved, so this shouldn't be preserved, - // either. - return false; - } - Node typeElt = JavaParserUtil.getEnclosingClassLike(method); - - if (typeElt instanceof EnumDeclaration) { - EnumDeclaration asEnum = (EnumDeclaration) typeElt; - Set parents = - convertToResolvedReferenceTypes(asEnum.getImplementedTypes()); - - return overridesAnInterfaceMethodImpl(parents, signature); - } else if (typeElt instanceof ClassOrInterfaceDeclaration) { - ClassOrInterfaceDeclaration asClass = (ClassOrInterfaceDeclaration) typeElt; - - // Get directly implemented interfaces as well as types implemented through parent classes - Set parents = - convertToResolvedReferenceTypes(asClass.getImplementedTypes()); - parents.addAll(convertToResolvedReferenceTypes(asClass.getExtendedTypes())); - - return overridesAnInterfaceMethodImpl(parents, signature); - } else { - throw new RuntimeException( - "unexpected enclosing structure " + typeElt + " for method " + method); - } - } - - /** - * Adds all resolvable ancestors (interfaces, superclasses) to the usedTypeElements set. It is - * intended to be used when the type is not already present in usedTypeElements, but there is no - * harm in calling this elsewhere. - * - * @param resolvedType the reference type to add its ancestors - */ - private void addAllResolvableAncestors(ResolvedReferenceType resolvedType) { - // If this method is called, this type is not used anywhere else except in this location - // Therefore, its inherited types (if solvable) should be preserved since it was - // not able to be preserved elsewhere. - for (ResolvedReferenceType implementation : - getAllImplementations(new HashSet<>(resolvedType.getAllAncestors()))) { - usedTypeElements.add(implementation.getQualifiedName()); - } - } - - /** - * Helper method for overridesAnInterfaceMethod, to allow the same code to be shared in the enum - * and class cases. - * - * @param implementedTypes the types of the implemented interfaces/classes - * @param signature the signature we're looking for - * @return see {@link #overridesAnInterfaceMethod(MethodDeclaration)} - */ - private boolean overridesAnInterfaceMethodImpl( - Set implementedTypes, String signature) { - // Classes may exist in this collection; their primary purpose is to exclude a method - // if a concrete method declaration exists - Collection allImplementedTypes = getAllImplementations(implementedTypes); - - boolean result = false; - - for (ResolvedReferenceType resolvedInterface : allImplementedTypes) { - // This boolean is important to distinguish between the case of - // an interface that's in the input/output (and therefore could change) - // and an interface that's not, such as java.util.Set from the JDK. For - // the latter, we need to preserve required overrides in all cases, even if - // they are not used. For the former, we only need to preserve required overrides - // if the method is actually invoked (if not, it will be removed from the interface - // elsewhere). - boolean inOutput = - this.existingClassesToFilePath.containsKey(resolvedInterface.getQualifiedName()); - - // It's necessary to viewpoint-adapt the type parameters so that the signature we're looking - // for matches the one that we'll find in the interface's definition. This code - // substitutes type variables in reverse: the target signature is adjusted to match - // the view of the type parameters from the perspective of the interface. For example, - // if the implemented interface is Set, this code will change the target signature - // add(V) to add(E), because in the definition of java.util.Set the type variable is - // called E. - ResolvedTypeParametersMap typeParametersMap = resolvedInterface.typeParametersMap(); - String targetSignature = signature; - for (String name : typeParametersMap.getNames()) { - String interfaceViewpointName = name.substring(name.lastIndexOf('.') + 1); - String localViewpointName = typeParametersMap.getValueBySignature(name).get().describe(); - // Escape localViewpointName in case it contains special regex characters like [] - // if the type is an array, for example - localViewpointName = Pattern.quote(localViewpointName); - targetSignature = targetSignature.replaceAll(localViewpointName, interfaceViewpointName); - } - // Type parameters in the types are erased (as they would be by javac when doing method - // dispatching). - // This means e.g. that a parameter with the type java.util.Collection will become - // java.util.Collection - // (i.e., a raw type). Note though that this doesn't mean there are no type variables in the - // signature: - // add(E) is still add(E). - targetSignature = JavaParserUtil.erase(targetSignature); - - for (ResolvedMethodDeclaration methodInInterface : - resolvedInterface.getAllMethodsVisibleToInheritors()) { - try { - if (JavaParserUtil.erase(methodInInterface.getSignature()).equals(targetSignature)) { - if (methodInInterface.isAbstract()) { - // once we've found the correct method, we return to whether we - // control it or not. If we don't, it must be preserved. If we do, then we only - // preserve it if the PrunerVisitor won't remove it. - if (!inOutput || usedMembers.contains(methodInInterface.getQualifiedSignature())) { - // Do not immediately return; if two ancestors, unincluded interfaces are present, - // one with a method declaration and one without, we need to return false even if - // this may be true (depends on which method is traversed first) - result = true; - continue; - } - } else if (!inOutput) { - // If we can't control the method, and there's a definition provided, we can safely - // remove all overridden versions - return false; - } - } - } catch (UnsolvedSymbolException ex) { - // since we are going through all ancestor interfaces/abstract classes, we should - // expect that some method signature cannot be resolved. if this is the case, then - // it's definitely not the method we're looking for. - continue; - } - } - } - // if we don't find an overridden method in any of the implemented interfaces, return false. - // however, if this method only implements abstract methods we can't control, then return true. - return result; - } - - /** - * Helper method to convert ClassOrInterfaceTypes to ResolvedReferenceTypes - * - * @param types A List of interface/class types to convert - * @return A set of ResolvedReferenceTypes representing the resolved input types - */ - private static Set convertToResolvedReferenceTypes( - List types) { - Set resolvedTypes = new HashSet<>(); - - for (ClassOrInterfaceType type : types) { - try { - ResolvedReferenceType resolved = - JavaParserUtil.classOrInterfaceTypeToResolvedReferenceType(type); - resolvedTypes.add(resolved); - } catch (UnsolvedSymbolException | UnsupportedOperationException ex) { - // In this case, we're implementing an interface that we don't control - // or that will not be preserved. - continue; - } - } - - return resolvedTypes; - } - - /** - * Gets all interface implementations of a List of ClassOrInterfaceTypes, including those of - * ancestors. This method is intended to be used for interface / class implementations only (i.e. - * pass in ClassOrInterfaceDeclaration.getImplementedTypes() or getExtendedTypes()). - * - * @param toTraverse A List of resolved reference types to find all ancestors - * @return A Collection of ResolvedReferenceTypes containing all ancestors - */ - private static Collection getAllImplementations( - Set toTraverse) { - Map qualifiedNameToType = new HashMap<>(); - while (!toTraverse.isEmpty()) { - Set newToTraverse = new HashSet<>(); - for (ResolvedReferenceType type : toTraverse) { - if (!qualifiedNameToType.containsKey(type.getQualifiedName())) { - qualifiedNameToType.put(type.getQualifiedName(), type); - for (ResolvedReferenceType implemented : type.getAllAncestors()) { - newToTraverse.add(implemented); - } - } - } - toTraverse.clear(); - toTraverse = newToTraverse; - } - - return qualifiedNameToType.values(); - } - - /** - * Given a MethodDeclaration, this method returns the method that it overrides, if one exists in - * one of its super classes. If one does not exist, it returns null. - * - * @param methodDeclaration the method declaration to check - * @return the method that this method overrides, if one exists in a superclass. Null if no such - * method exists. - */ - public static @Nullable ResolvedMethodDeclaration getOverriddenMethodInSuperClass( - MethodDeclaration methodDeclaration) { - // just a method signature, no need to check for overriding. - if (methodDeclaration.getBody().isEmpty()) { - return null; - } - BlockStmt methodBody = methodDeclaration.getBody().get(); - // JavaParser does not support solving overriding, but it does support solving super - // expressions. So we make a temporary super expression to figure out if this current method is - // overriding. - MethodCallExpr superCall = new MethodCallExpr(); - superCall.setName(methodDeclaration.getName()); - NodeList parameters = methodDeclaration.getParameters(); - for (Parameter parameter : parameters) { - superCall.addArgument(parameter.getNameAsString()); - } - superCall.setScope(new SuperExpr()); - methodBody.addStatement(superCall); - ResolvedMethodDeclaration resolvedSuperCall = null; - try { - resolvedSuperCall = superCall.resolve(); - } catch (Exception e) { - // The current method is not overriding, thus the super call is unresolved. - // This catch block is necessary to avoid crashes due to ignored catch blocks. A single - // remove() call is not enough to remove a MethodCallExpr. - superCall.remove(); - } - JavaParserUtil.removeNode(superCall); - return resolvedSuperCall; - } -} diff --git a/src/main/java/org/checkerframework/specimin/PrunerVisitor.java b/src/main/java/org/checkerframework/specimin/PrunerVisitor.java deleted file mode 100644 index 69782a088..000000000 --- a/src/main/java/org/checkerframework/specimin/PrunerVisitor.java +++ /dev/null @@ -1,560 +0,0 @@ -package org.checkerframework.specimin; - -import com.github.javaparser.StaticJavaParser; -import com.github.javaparser.ast.Node; -import com.github.javaparser.ast.NodeList; -import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration; -import com.github.javaparser.ast.body.ConstructorDeclaration; -import com.github.javaparser.ast.body.EnumConstantDeclaration; -import com.github.javaparser.ast.body.EnumDeclaration; -import com.github.javaparser.ast.body.FieldDeclaration; -import com.github.javaparser.ast.body.InitializerDeclaration; -import com.github.javaparser.ast.body.MethodDeclaration; -import com.github.javaparser.ast.body.VariableDeclarator; -import com.github.javaparser.ast.expr.AnnotationExpr; -import com.github.javaparser.ast.expr.BooleanLiteralExpr; -import com.github.javaparser.ast.expr.CharLiteralExpr; -import com.github.javaparser.ast.expr.DoubleLiteralExpr; -import com.github.javaparser.ast.expr.Expression; -import com.github.javaparser.ast.expr.IntegerLiteralExpr; -import com.github.javaparser.ast.expr.LongLiteralExpr; -import com.github.javaparser.ast.expr.NullLiteralExpr; -import com.github.javaparser.ast.stmt.BlockStmt; -import com.github.javaparser.ast.stmt.Statement; -import com.github.javaparser.ast.type.ClassOrInterfaceType; -import com.github.javaparser.ast.type.PrimitiveType; -import com.github.javaparser.ast.type.Type; -import com.github.javaparser.ast.type.TypeParameter; -import com.github.javaparser.ast.visitor.Visitable; -import com.github.javaparser.resolution.UnsolvedSymbolException; -import com.github.javaparser.resolution.declarations.ResolvedConstructorDeclaration; -import com.github.javaparser.resolution.declarations.ResolvedEnumConstantDeclaration; -import com.github.javaparser.resolution.declarations.ResolvedMethodDeclaration; -import com.github.javaparser.resolution.declarations.ResolvedReferenceTypeDeclaration; -import com.github.javaparser.resolution.types.ResolvedReferenceType; -import com.github.javaparser.resolution.types.ResolvedType; -import java.util.HashSet; -import java.util.Iterator; -import java.util.Map; -import java.util.NoSuchElementException; -import java.util.Set; -import org.checkerframework.checker.nullness.qual.Nullable; - -/** - * This visitor removes every member in the compilation unit that is not a member of its {@link - * #targetMethods} set or {@link #usedMembers} set. It also deletes the bodies of all methods and - * replaces them with "throw new java.lang.Error();" or remove the initializers of fields (minimized - * if the field is final) within the {@link #usedMembers} set. - */ -public class PrunerVisitor extends SpeciminStateVisitor { - - /** - * This boolean tracks whether the element currently being visited is inside an interface - * annotated as @FunctionalInterface. This annotation is added to allow lambdas in target methods - * to be passed to other methods, so the methods in such interfaces need to be preserved. - */ - private boolean insideFunctionalInterface = false; - - /** - * JavaParser is not perfect. Sometimes it can't solve resolved method calls if they have - * complicated type variables. We keep track of these stuck method calls and preserve them anyway. - */ - private final Set resolvedYetStuckMethodCall; - - /** This map connects a class and its unresolved interface. */ - private final Map classAndUnresolvedInterface; - - /** - * Creates the pruner. All members this pruner encounters other than those in its input sets will - * be removed entirely. - * - * @param previousVisitor the previous visitor to run, from whence state should be copied - * @param resolvedYetStuckMethodCall set of methods that are resolved yet can not be solved by - * JavaParser - * @param classAndUnresolvedInterface connects a class to its corresponding unresolved interface - */ - public PrunerVisitor( - SpeciminStateVisitor previousVisitor, - Set resolvedYetStuckMethodCall, - Map classAndUnresolvedInterface) { - super(previousVisitor); - this.classAndUnresolvedInterface = classAndUnresolvedInterface; - Set toRemove = new HashSet<>(); - for (String classUsedByTargetMethods : usedTypeElements) { - if (classUsedByTargetMethods.contains("<")) { - toRemove.add(classUsedByTargetMethods); - } - } - for (String s : toRemove) { - usedTypeElements.remove(s); - String withoutAngleBrackets = s.substring(0, s.indexOf("<")); - usedTypeElements.add(withoutAngleBrackets); - } - this.resolvedYetStuckMethodCall = resolvedYetStuckMethodCall; - } - - /** - * Helper method to check if the given fully-qualified class name is used as a parameter type by - * any of the methods in {@link #usedMembers}. - * - * @param classFullName a fully-qualified class name - * @return true if this type name is a parameter of a used method - */ - public boolean isUsedMethodParameterType(String classFullName) { - for (String member : usedMembers) { - int openParen = member.indexOf('('); - int closeParen = member.lastIndexOf(')'); - - if (openParen == -1 || closeParen == -1) { - continue; - } - - String parameters = member.substring(openParen + 1, closeParen); - - int index = parameters.indexOf(classFullName); - if (index == -1) { - continue; - } else if (index == 0) { - if (parameters.length() == classFullName.length()) { - return true; - } - char after = parameters.charAt(index + classFullName.length()); - // Check to see if it's generic or an array, or if it matches the first parameter - if (after == '<' || after == ',' || after == '[') { - return true; - } - } - // Check to see if it is a parameter - else if (index > 1 && parameters.substring(index - 2, index).equals(", ")) { - char after = parameters.charAt(index + classFullName.length()); - if (after == '<' || after == ',' || after == '[') { - return true; - } - } - } - return false; - } - - /** - * This method removes any implemented interfaces that are not used and therefore shouldn't be - * preserved from the declaration of a class, interface, or enum. The argument should be produced - * by calling the appropriate {@code getImplementedTypes()} method on the declaration, and after - * calling this method there should be a call to {@code setImplementedTypes} on the declaration so - * that its changes take effect. Side-effects its argument. - * - * @param qualifiedName the fully-qualified name of the class/interface/enum whose interfaces - * might be removed - * @param implementedInterfaces the list of implemented interfaces to consider. After this method - * terminates, this list will have been side-effected to remove any interfaces that should not - * be preserved. - */ - private void removeUnusedInterfacesHelper( - String qualifiedName, NodeList implementedInterfaces) { - Iterator iterator = implementedInterfaces.iterator(); - while (iterator.hasNext()) { - ClassOrInterfaceType interfaceType = iterator.next(); - try { - String typeFullName = - JavaParserUtil.classOrInterfaceTypeToResolvedReferenceType(interfaceType) - .getQualifiedName(); - - // Never remove java.lang.AutoCloseable, because it will create compilation - // errors at try-with-resources statements. - if (typeFullName.equals("java.lang.AutoCloseable")) { - continue; - } - if (!usedTypeElements.contains(typeFullName)) { - iterator.remove(); - continue; - } - // all unresolvable interfaces that need to be removed belong to the Java package. - if (!JavaLangUtils.inJdkPackage(typeFullName)) { - continue; - } - for (String classNeedInterfaceRemoved : classAndUnresolvedInterface.keySet()) { - // since classNeedInterfaceRemoved can be in the form of a simple name - if (qualifiedName.endsWith(classNeedInterfaceRemoved)) { - if (classAndUnresolvedInterface - .get(classNeedInterfaceRemoved) - .equals(interfaceType.getNameAsString())) { - // This code assumes that the likelihood of two different classes with the same - // simple name implementing the same interface is low. - iterator.remove(); - continue; - } - } - } - } catch (UnsolvedSymbolException e) { - iterator.remove(); - } - } - } - - @Override - public Visitable visit(EnumDeclaration decl, Void p) { - String qualifiedName = decl.resolve().getQualifiedName(); - if (!usedTypeElements.contains(qualifiedName)) { - decl.remove(); - return decl; - } - NodeList implementedInterfaces = decl.getImplementedTypes(); - removeUnusedInterfacesHelper(qualifiedName, implementedInterfaces); - decl.setImplementedTypes(implementedInterfaces); - - return super.visit(decl, p); - } - - @Override - public Visitable visit(ClassOrInterfaceDeclaration decl, Void p) { - boolean oldInsideFunctionalInterface = insideFunctionalInterface; - @Nullable AnnotationExpr functionInterfaceAnnotationExpr = null; - for (AnnotationExpr anno : decl.getAnnotations()) { - if ("FunctionalInterface".equals(anno.getNameAsString())) { - insideFunctionalInterface = true; - functionInterfaceAnnotationExpr = anno; - } - } - if (functionInterfaceAnnotationExpr != null) { - // @FunctionalInterface is optional, so we will remove it to avoid possible compilation - // errors. - functionInterfaceAnnotationExpr.remove(); - } - decl = minimizeTypeParameters(decl); - String classQualifiedName = decl.resolve().getQualifiedName(); - if (!usedTypeElements.contains(classQualifiedName) - && !isUsedMethodParameterType(classQualifiedName)) { - decl.remove(); - return decl; - } - if (!decl.isInterface()) { - NodeList implementedInterfaces = decl.getImplementedTypes(); - removeUnusedInterfacesHelper(classQualifiedName, implementedInterfaces); - decl.setImplementedTypes(implementedInterfaces); - } - Visitable result = super.visit(decl, p); - insideFunctionalInterface = oldInsideFunctionalInterface; - return result; - } - - @Override - public Visitable visit(InitializerDeclaration decl, Void p) { - decl.remove(); - return decl; - } - - @Override - public Visitable visit(EnumConstantDeclaration enumConstantDeclaration, Void p) { - ResolvedEnumConstantDeclaration resolved; - try { - resolved = enumConstantDeclaration.resolve(); - } catch (UnsolvedSymbolException | UnsupportedOperationException e) { - JavaParserUtil.removeNode(enumConstantDeclaration); - return enumConstantDeclaration; - } - if (!usedMembers.contains( - resolved.getType().describe() + "." + enumConstantDeclaration.getNameAsString())) { - JavaParserUtil.removeNode(enumConstantDeclaration); - } - return enumConstantDeclaration; - } - - @Override - public Visitable visit(MethodDeclaration methodDecl, Void p) { - String signature; - try { - // resolved() will only check if the return type is solvable - // getQualifiedSignature() will also check if the parameters are solvable - signature = methodDecl.resolve().getQualifiedSignature(); - } catch (UnsolvedSymbolException e) { - // The current class is employed by the target methods, although not all of its members are - // utilized. It's not surprising for unused members to remain unresolved. - methodDecl.remove(); - return methodDecl; - } - - if (targetMethods.contains(signature)) { - return super.visit(methodDecl, p); - } - - if (insideFunctionalInterface && usedMembers.contains(signature)) { - if (methodDecl.getBody().isPresent()) { - // avoid introducing unsolved symbols into the final output. - methodDecl.setBody(StaticJavaParser.parseBlock("{ throw new java.lang.Error(); }")); - } - return methodDecl; - } - - if (usedMembers.contains(signature) || isAResolvedYetStuckMethod(methodDecl)) { - boolean isMethodInsideInterface = isInsideInterface(methodDecl); - // do nothing if methodDecl is just a method signature in a class. - if (methodDecl.getBody().isPresent() || isMethodInsideInterface) { - methodDecl.setBody(StaticJavaParser.parseBlock("{ throw new java.lang.Error(); }")); - // static and default keywords can not be together. - if (isMethodInsideInterface && !methodDecl.isStatic()) { - methodDecl.setDefault(true); - } - } - return methodDecl; - } - - // if insideTargetMethod is true, this current method declaration belongs to an anonnymous - // class inside the target method. - if (!insideTargetMember) { - methodDecl.remove(); - } - return methodDecl; - } - - @Override - public Visitable visit(ConstructorDeclaration constructorDecl, Void p) { - String qualifiedSignature; - try { - // resolved() will only check if the return type is solvable - // getQualifiedSignature() will also check if the parameters are solvable - qualifiedSignature = constructorDecl.resolve().getQualifiedSignature(); - } catch (RuntimeException e) { - // The current class is employed by the target methods, although not all of its members are - // utilized. It's not surprising for unused members to remain unresolved. - // If this constructor is from the parent of the current class, and it is not resolved, we - // will get a RuntimeException, otherwise just a UnsolvedSymbolException. - constructorDecl.remove(); - return constructorDecl; - } - - if (targetMethods.contains(qualifiedSignature)) { - return super.visit(constructorDecl, p); - } - - // TODO: we should be cleverer about whether to preserve the constructors of - // enums, but right now we don't remove any enum constants in related classes, so - // we need to preserve all constructors to retain compilability. - if (usedMembers.contains(qualifiedSignature) || JavaParserUtil.isInEnum(constructorDecl)) { - if (!needToPreserveSuperOrThisCall(constructorDecl.resolve())) { - constructorDecl.setBody(StaticJavaParser.parseBlock("{ throw new java.lang.Error(); }")); - return constructorDecl; - } - - NodeList bodyStatement = constructorDecl.getBody().getStatements(); - if (bodyStatement.isEmpty()) { - return constructorDecl; - } - Statement firstStatement = bodyStatement.get(0); - if (firstStatement.isExplicitConstructorInvocationStmt()) { - BlockStmt minimized = new BlockStmt(); - minimized.addStatement(firstStatement); - constructorDecl.setBody(minimized); - return constructorDecl; - } - - // not sure if we will ever get to this line. So this line is merely for the peace of mind. - constructorDecl.setBody(StaticJavaParser.parseBlock("{ throw new java.lang.Error(); }")); - return constructorDecl; - } - - constructorDecl.remove(); - return constructorDecl; - } - - @Override - public Visitable visit(FieldDeclaration fieldDecl, Void p) { - if (insideTargetMember) { - return super.visit(fieldDecl, p); - } - - boolean isFinal = fieldDecl.isFinal(); - String classFullName = JavaParserUtil.getEnclosingClassName(fieldDecl); - Iterator iterator = fieldDecl.getVariables().iterator(); - while (iterator.hasNext()) { - VariableDeclarator declarator = iterator.next(); - try { - declarator.resolve(); - } catch (UnsolvedSymbolException e) { - // The current class is employed by the target methods, although not all of its members are - // utilized. It's not surprising for unused members to remain unresolved. - declarator.remove(); - continue; - } - String varFullName = classFullName + "#" + declarator.getNameAsString(); - - if (targetFields.contains(varFullName)) { - continue; - } else if (usedMembers.contains(varFullName)) { - if (isFinal) { - if (!fieldsAssignedByTargetCtors.contains(varFullName)) { - declarator.removeInitializer(); - declarator.setInitializer(getBasicInitializer(declarator.getType())); - } - } else { - declarator.removeInitializer(); - } - } else { - iterator.remove(); - } - } - - // if all the declarators were removed, remove this field, too - if (fieldDecl.getVariables().isEmpty()) { - fieldDecl.remove(); - } - - return super.visit(fieldDecl, p); - } - - /** - * Check if this method is one of the method calls used by target methods that are resolved yet - * can not be solved by JavaParser. - * - * @param method a method - * @return true if the above statement is true. - */ - private boolean isAResolvedYetStuckMethod(MethodDeclaration method) { - ResolvedMethodDeclaration decl = method.resolve(); - String methodQualifiedName = decl.getQualifiedSignature(); - String methodSimpleName = method.getNameAsString(); - int numberOfParams = decl.getNumberOfParams(); - boolean isVarArgs = numberOfParams == 0 ? false : decl.getLastParam().isVariadic(); - for (String stuckMethodCall : resolvedYetStuckMethodCall) { - if (stuckMethodCall.contains("@")) { - // The stuck method call contains an @ iff it is in the stuck method call - // list because we couldn't determine its qualified signature from the context - // in which it was called (e.g., a method called on a lambda parameter). - // The format is the name of the method followed by an @ followed by the - // number of arguments at the call site. Preserve anything that matches. - String stuckMethodName = stuckMethodCall.substring(0, stuckMethodCall.indexOf('@')); - int stuckMethodNumberOfParams = - Integer.parseInt(stuckMethodCall.substring(stuckMethodCall.indexOf('@') + 1)); - if (methodSimpleName.equals(stuckMethodName) - && ((!isVarArgs && numberOfParams == stuckMethodNumberOfParams) - || (isVarArgs && numberOfParams <= stuckMethodNumberOfParams))) { - return true; - } - } else if (methodQualifiedName.startsWith(stuckMethodCall)) { - return true; - } - } - return false; - } - - /** - * Creates a basic initializer expression for a specified field type. The way the initial value is - * chosen is based on the document of the Java Language: - * https://docs.oracle.com/javase/specs/jls/se7/html/jls-4.html#jls-4.12.5 - * - * @param fieldType The type for which to generate the basic initializer. - * @return An Expression representing the basic initializer for the given field type. - */ - private Expression getBasicInitializer(Type fieldType) { - if (!fieldType.isPrimitiveType()) { - return new NullLiteralExpr(); - } - - PrimitiveType.Primitive primitiveType = ((PrimitiveType) fieldType).getType(); - switch (primitiveType) { - case BOOLEAN: - return new BooleanLiteralExpr(false); - case INT: - return new IntegerLiteralExpr("0"); - case LONG: - return new LongLiteralExpr("0L"); - case FLOAT: - return new DoubleLiteralExpr("0.0f"); - case DOUBLE: - return new DoubleLiteralExpr("0.0"); - case BYTE: - return new IntegerLiteralExpr("0"); - case SHORT: - return new IntegerLiteralExpr("0"); - case CHAR: - return new CharLiteralExpr("'\u0000'"); - default: - throw new RuntimeException("Unexpected primitive type: " + fieldType); - } - } - - /** - * Given the declaration of a class, this method returns the updated declaration with the unused - * type bounds of the type parameters removed. - * - * @param decl the declaration of a class. - * @return that declaration with unused type bounds of type parameters removed. - */ - private ClassOrInterfaceDeclaration minimizeTypeParameters(ClassOrInterfaceDeclaration decl) { - NodeList typeParameterList = decl.getTypeParameters(); - NodeList updatedTypeParameterList = new NodeList<>(); - for (TypeParameter typeParameter : typeParameterList) { - typeParameter = typeParameter.setTypeBound(getUsedTypesOnly(typeParameter.getTypeBound())); - updatedTypeParameterList.add(typeParameter); - } - return decl.setTypeParameters(updatedTypeParameterList); - } - - /** - * Given a NodeList of types, this method removes those types not used by target methods. - * - * @param inputList a NodeList of ClassOrInterfaceType instances. - * @return the updated list with unused types removed. - */ - private NodeList getUsedTypesOnly( - NodeList inputList) { - NodeList usedTypeOnly = new NodeList<>(); - for (ClassOrInterfaceType type : inputList) { - ResolvedType resolvedType; - try { - resolvedType = type.resolve(); - } catch (UnsolvedSymbolException | IllegalStateException e) { - continue; - } - if (usedTypeElements.contains(resolvedType.asReferenceType().getQualifiedName())) { - usedTypeOnly.add(type); - } - } - return usedTypeOnly; - } - - /** - * Check if a node is inside an interface. - * - * @param node the node to be checked. - * @return true if node is inside an interface. - */ - private boolean isInsideInterface(Node node) { - if (node.getParentNode().isEmpty()) { - return false; - } - Node parentNode = node.getParentNode().get(); - if (parentNode instanceof ClassOrInterfaceDeclaration) { - return ((ClassOrInterfaceDeclaration) parentNode).isInterface(); - } - return isInsideInterface(parentNode); - } - - /** - * Checks if a constructor, used by target methods, needs to have its explicit constructor - * invocation preserved. If a constructor is from a class that extends another class, and if the - * extended class's constructor is also used by target methods, then the current constructor - * should have its explicit invocation preserved, instead of being emptied out completely. - * - * @param constructorDeclaration The constructor used by the target methods. - * @return {@code true} if the constructor needs to be have its explicit constructor invocation - * preserved, {@code false} otherwise. - */ - private boolean needToPreserveSuperOrThisCall( - ResolvedConstructorDeclaration constructorDeclaration) { - ResolvedReferenceTypeDeclaration enclosingClass = constructorDeclaration.declaringType(); - for (ResolvedReferenceType extendedClass : enclosingClass.getAncestors()) { - try { - for (ResolvedConstructorDeclaration constructorOfExtendedClass : - extendedClass.getTypeDeclaration().get().getConstructors()) { - if (usedMembers.contains(constructorOfExtendedClass.getQualifiedSignature())) { - return true; - } - } - } - // NoSuchElementException is for cases where the type declaration is not available. - catch (UnsolvedSymbolException | UnsupportedOperationException | NoSuchElementException e) { - return false; - } - } - return false; - } -} diff --git a/src/main/java/org/checkerframework/specimin/Slicer.java b/src/main/java/org/checkerframework/specimin/Slicer.java new file mode 100644 index 000000000..88f50d959 --- /dev/null +++ b/src/main/java/org/checkerframework/specimin/Slicer.java @@ -0,0 +1,515 @@ +package org.checkerframework.specimin; + +import com.github.javaparser.StaticJavaParser; +import com.github.javaparser.ast.CompilationUnit; +import com.github.javaparser.ast.Node; +import com.github.javaparser.ast.NodeList; +import com.github.javaparser.ast.body.CallableDeclaration; +import com.github.javaparser.ast.body.FieldDeclaration; +import com.github.javaparser.ast.body.MethodDeclaration; +import com.github.javaparser.ast.body.TypeDeclaration; +import com.github.javaparser.ast.body.VariableDeclarator; +import com.github.javaparser.ast.expr.AssignExpr; +import com.github.javaparser.ast.expr.Expression; +import com.github.javaparser.ast.expr.FieldAccessExpr; +import com.github.javaparser.ast.expr.NameExpr; +import com.github.javaparser.ast.expr.ObjectCreationExpr; +import com.github.javaparser.ast.nodeTypes.NodeWithArguments; +import com.github.javaparser.ast.stmt.BlockStmt; +import com.github.javaparser.ast.type.ClassOrInterfaceType; +import com.github.javaparser.ast.type.TypeParameter; +import com.github.javaparser.ast.type.UnknownType; +import com.github.javaparser.resolution.Resolvable; +import com.github.javaparser.resolution.UnsolvedSymbolException; +import com.github.javaparser.resolution.declarations.ResolvedFieldDeclaration; +import com.github.javaparser.resolution.declarations.ResolvedValueDeclaration; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Deque; +import java.util.HashSet; +import java.util.IdentityHashMap; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.specimin.unsolved.UnsolvedGenerationResult; +import org.checkerframework.specimin.unsolved.UnsolvedSymbolAlternates; +import org.checkerframework.specimin.unsolved.UnsolvedSymbolGenerator; + +/** + * Slices a program, given an initial worklist and a type rule dependency map. This class cannot be + * instantiated; instead, use {@link #slice(TypeRuleDependencyMap, Deque, UnsolvedSymbolGenerator, + * Map)} to use this class. + */ +public class Slicer { + /** + * The slice of nodes. + * + *

We cannot use HashSet here; when nodes' children are modified, {@code slice.contains} + * no longer returns true, even if {@code equals} evaluates to true, {@code hashCode} returns the + * same value, and {@code ==} yields the same reference. + * + *

An {@code IdentityHashMap} is safe to use because we use the same compilation units + * everywhere, so all corresponding nodes will have the same reference. + */ + private final Set slice = Collections.newSetFromMap(new IdentityHashMap<>()); + + /** The slice of generated symbols. */ + private final Set> generatedSymbolSlice = new LinkedHashSet<>(); + + /** The Nodes that need to be processed in the main algorithm. */ + private final Deque worklist; + + /** + * A secondary worklist to use to add information to generated symbols, once all unsolved symbols + * are guaranteed to be generated. Contains only select Nodes, determined by {@link + * UnsolvedSymbolGenerator#needToPostProcess(Node)}. + */ + private final Deque postProcessingWorklist = new ArrayDeque<>(); + + /** The result of the slice, not including generated symbols. */ + private final Set resultCompilationUnits = new HashSet<>(); + + /** The unsolved symbol generator. */ + private final UnsolvedSymbolGenerator unsolvedSymbolGenerator; + + /** The type rule dependency map. */ + private final TypeRuleDependencyMap typeRuleDependencyMap; + + /** The map of fully-qualified names to their compilation units. */ + private final Map fqnToCompilationUnits; + + /** + * Creates a new instance of {@link Slicer}. + * + * @param typeRuleDependencyMap The type rule dependency map to use in the slice. + * @param worklist The worklist to use, already populated with target members and their bodies. + * @param unsolvedSymbolGenerator The unsolved symbol generator to use. + * @param fqnToCompilationUnits The fully qualified name to compilation units map. + */ + private Slicer( + TypeRuleDependencyMap typeRuleDependencyMap, + Deque worklist, + UnsolvedSymbolGenerator unsolvedSymbolGenerator, + Map fqnToCompilationUnits) { + this.typeRuleDependencyMap = typeRuleDependencyMap; + this.worklist = worklist; + this.unsolvedSymbolGenerator = unsolvedSymbolGenerator; + this.fqnToCompilationUnits = fqnToCompilationUnits; + } + + /** + * Slices a program based on the type rule dependency map and an initial worklist. Returns a + * {@link SliceResult} which contains sliced compilation units and also generated unsolved + * symbols. + * + * @param typeRuleDependencyMap The type rule dependency map to use in the slice. + * @param worklist The worklist to use, already populated with target members and their bodies. + * @param unsolvedSymbolGenerator The unsolved symbol generator to use. + * @param fqnToCompilationUnits The map of type FQNs to their compilation units. + * @return A {@link SliceResult} representing the output of the slice. + */ + public static SliceResult slice( + TypeRuleDependencyMap typeRuleDependencyMap, + Deque worklist, + UnsolvedSymbolGenerator unsolvedSymbolGenerator, + Map fqnToCompilationUnits) { + Slicer slicer = + new Slicer(typeRuleDependencyMap, worklist, unsolvedSymbolGenerator, fqnToCompilationUnits); + + slicer.buildSlice(); + + unsolvedSymbolGenerator.generateAllAlternatesBasedOnSuperTypeRelationships(); + + Set dependentSlice = new HashSet<>(); + // Use getGeneratedSymbols() instead of Slicer.generatedSymbolSlice here because we want to + // include + // nodes which are included by generated symbols that are conditionally included as well, not + // just + // those in the slice + for (UnsolvedSymbolAlternates gen : + slicer.unsolvedSymbolGenerator.getGeneratedSymbols().values()) { + for (Node node : gen.getDependentNodes()) { + if (!slicer.slice.contains(node)) { + dependentSlice.add(node); + } + // These nodes may be something like a method declaration; we need to call the + // type rule dependency map once to get its keywords to make sure it's not removed + slicer.slice.add(node); + slicer.slice.addAll(typeRuleDependencyMap.getRelevantElements(node)); + } + } + + slicer.prune(); + + return new SliceResult( + slicer.resultCompilationUnits, slicer.generatedSymbolSlice, dependentSlice); + } + + /** + * The main slicing algorithm. Mutates the compilation units in {@link #resultCompilationUnits} + * and trims all unused nodes, while also adding needed generated symbols to the result. + */ + private void buildSlice() { + // Step 1: build the slice; see which nodes to keep + while (!worklist.isEmpty()) { + Node element = worklist.removeLast(); + handleElement(element); + } + + generatedSymbolSlice.addAll(unsolvedSymbolGenerator.clearMethodsWithNull()); + + if (!generatedSymbolSlice.isEmpty()) { + // Step 2: Add more information to generated symbols based on context + Iterator ppwIterator = postProcessingWorklist.iterator(); + while (ppwIterator.hasNext()) { + Node element = ppwIterator.next(); + UnsolvedGenerationResult result = unsolvedSymbolGenerator.addInformation(element); + generatedSymbolSlice.addAll(result.toAdd()); + generatedSymbolSlice.removeAll(result.toRemove()); + } + } + } + + /** Prunes all unused elements based on the slice. */ + private void prune() { + // Step 3: go through each compilation unit and remove unused nodes + for (CompilationUnit cu : resultCompilationUnits) { + // If a non-primary class is preserved, the primary class still must be preserved, + // even if all its nodes were removed + slice.add(cu.getPrimaryType().get()); + + removeNonSliceNodesFromCompilationUnit(cu); + } + } + + /** + * Helper method for each node in the worklist. Generates a symbol if needed, otherwise it simply + * adds it and the results of a call to the type rule dependency map to the slice. + * + * @param node The node to handle + */ + private void handleElement(Node node) { + if (slice.contains(node)) { + return; + } + + // TypeParameter#resolve() throws an UnsupportedOperationException. + // Since we don't need to resolve the type parameter definition, it is + // safe to skip this step, since all child nodes will be added to the + // worklist anyway. + + // UnknownType#resolve() throws an IllegalArgumentException (see docs, + // #convertToUsage(Context)). + // https://www.javadoc.io/doc/com.github.javaparser/javaparser-core/latest/com/github/javaparser/ast/type/UnknownType.html + if (node instanceof Resolvable + && !(node instanceof TypeParameter || node instanceof UnknownType)) { + Resolvable asResolvable = (Resolvable) node; + + boolean generateUnsolvedSymbol = false; + Object resolved = null; + try { + // Do not call resolve on a FieldDeclaration, since it will throw an + // UnsupportedOperationException + // if the field has multiple declarators. We will resolve the declarators instead. + if (!(node instanceof FieldDeclaration)) { + // Resolve isn't perfect: methods/constructors, even if in the same file, will not resolve + // if there are unresolvable argument types + resolved = asResolvable.resolve(); + } + } catch (UnsupportedOperationException ex) { + // java.lang.UnsupportedOperationException: The type declaration cannot be found on + // constraint T + // Workaround for resolving methods/fields with a qualifier that is resolvable, but returns + // a lambda constraint type with a type parameter instead of a type + + // If not this case, then throw the error. + if (!(node instanceof Expression)) { + throw ex; + } + + resolved = + JavaParserUtil.tryFindCorrespondingDeclarationForConstraintQualifiedExpression( + (Expression) node); + + if (resolved == null) { + throw ex; + } + } catch (UnsolvedSymbolException ex) { + boolean shouldTryToResolve = true; + if (node instanceof ClassOrInterfaceType type && JavaParserUtil.isProbablyAPackage(type)) { + // We may encounter this if the user includes a FQN in their input, since the type rule + // dependency map returns the scope of the type, even if it's a package. + shouldTryToResolve = false; + generateUnsolvedSymbol = false; + } + + // Handle cases where a method/constructor call cannot be resolved because of unresolvable + // arguments, but its definition exists + CallableDeclaration potentiallyResolvableCallable = + node instanceof NodeWithArguments withArgs + ? JavaParserUtil.tryFindSingleCallableForNodeWithUnresolvableArguments( + withArgs, fqnToCompilationUnits) + : null; + + if (potentiallyResolvableCallable != null) { + try { + resolved = ((Resolvable) potentiallyResolvableCallable).resolve(); + shouldTryToResolve = false; + } catch (UnsolvedSymbolException e) { + // This should never happen + } + } + + if (shouldTryToResolve) { + // Calling resolve on a FieldAccessExpr/NameExpr that represents a type may also cause + // an UnsolvedSymbolException, even if the type is resolvable + if (node instanceof FieldAccessExpr || node instanceof NameExpr) { + if (!JavaParserUtil.isProbablyAPackage((Expression) node)) { + try { + resolved = ((Expression) node).calculateResolvedType(); + } catch (UnsolvedSymbolException ex2) { + generateUnsolvedSymbol = true; + } + } else { + generateUnsolvedSymbol = false; + } + } else { + generateUnsolvedSymbol = true; + } + } + + if (generateUnsolvedSymbol && node instanceof Expression expr) { + Object result = JavaParserUtil.tryResolveExpressionIfInAnonymousClass(expr); + if (result != null) { + resolved = result; + generateUnsolvedSymbol = false; + } + } + } + + if (resolved != null) { + generateUnsolvedSymbol = handleResolvedObject(node, resolved); + } + + if (generateUnsolvedSymbol) { + generatedSymbolSlice.addAll(unsolvedSymbolGenerator.inferContext(node)); + } + } + + slice.add(node); + worklist.addAll(typeRuleDependencyMap.getRelevantElements(node)); + + if (unsolvedSymbolGenerator.needToPostProcess(node)) { + postProcessingWorklist.add(node); + } + } + + /** + * Handles a resolved Object returned by {@code Resolvable.resolve()}. Helper method to be used + * by {@link #handleElement(Node)}. + * + * @param resolved The resolved object + * @return true if an unsolved symbol must be generated from this node (if a method is marked with + * {@code @Override} but no override is found); false otherwise + */ + private boolean handleResolvedObject(Node unresolved, @Nullable Object resolved) { + if (resolved == null) { + throw new RuntimeException("Unexpected null value in resolve() call"); + } + + List toAddToWorklist = typeRuleDependencyMap.getRelevantElements(resolved); + worklist.addAll(toAddToWorklist); + + // Since resolved declarations may reference another file, we need to add that compilation + // unit to the output + resultCompilationUnits.addAll( + toAddToWorklist.stream().map(n -> n.findCompilationUnit().get()).toList()); + + if (unresolved instanceof MethodDeclaration methodDecl + && (methodDecl.getAnnotationByName("Override").isPresent() + || (methodDecl.getParentNode().orElse(null) instanceof ObjectCreationExpr + && methodDecl.isPublic()))) { + return !toAddToWorklist.stream() + .anyMatch(n -> n instanceof MethodDeclaration && !n.equals(methodDecl)); + } + + return false; + } + + /** + * Removes unused nodes from a compilation unit. Use this instead of {@link + * #removeNonSliceNodes(Node)} because {@link CompilationUnit} has some quirks that prevents + * {@link CompilationUnit#getChildNodes()} from accessing anything but package/import + * declarations. + * + * @param cu The compilation unit + */ + private void removeNonSliceNodesFromCompilationUnit(CompilationUnit cu) { + List> typesCopy = new ArrayList<>(cu.getTypes()); + for (TypeDeclaration typeDecl : typesCopy) { + removeNonSliceNodes(typeDecl); + } + } + + /** + * Recursively removes all nodes not in the slice. + * + * @param node The node to slice + */ + private void removeNonSliceNodes(Node node) { + if (slice.contains(node)) { + List copy = new ArrayList<>(node.getChildNodes()); + for (Node child : copy) { + removeNonSliceNodes(child); + } + + // If we encounter an empty final field, try to see if we've included an assignment expression + // that sets it in the slice. If not, then we need to add a default initializer. + if (node instanceof VariableDeclarator fieldDeclarator + && fieldDeclarator.getInitializer().isEmpty() + && fieldDeclarator.getParentNode().get() instanceof FieldDeclaration fieldDecl + && fieldDecl.isFinal()) { + ResolvedFieldDeclaration resolved = (ResolvedFieldDeclaration) fieldDeclarator.resolve(); + + boolean isSet = + slice.stream() + .filter(n -> n instanceof AssignExpr) + .map(n -> (AssignExpr) n) + .filter( + assignExpr -> { + if (assignExpr.getTarget().isFieldAccessExpr()) { + try { + ResolvedValueDeclaration target = + assignExpr.getTarget().asFieldAccessExpr().resolve(); + + if (target.isField()) { + return target + .asField() + .declaringType() + .getQualifiedName() + .equals(resolved.declaringType().getQualifiedName()) + && target.getName().equals(resolved.getName()); + } + } catch (UnsolvedSymbolException e) { + return false; + } + } else if (assignExpr.getTarget().isNameExpr()) { + try { + ResolvedValueDeclaration target = + assignExpr.getTarget().asNameExpr().resolve(); + + if (target.isField()) { + return target + .asField() + .declaringType() + .getQualifiedName() + .equals(resolved.declaringType().getQualifiedName()) + && target.getName().equals(resolved.getName()); + } + return target.equals(resolved); + } catch (UnsolvedSymbolException e) { + return false; + } + } + + return false; + }) + .findFirst() + .isPresent(); + + if (!isSet) { + fieldDeclarator.setInitializer( + JavaParserUtil.getInitializerRHS(fieldDeclarator.getType().toString())); + } + } + } + // If a BlockStmt is being removed, it's a method/constructor to be trimmed + else if (node instanceof BlockStmt blockStmt + && node.getParentNode().get() instanceof CallableDeclaration callable) { + TypeDeclaration enclosing = JavaParserUtil.getEnclosingClassLike(node); + + boolean handled = false; + if (enclosing.isClassOrInterfaceDeclaration() && callable.isMethodDeclaration()) { + // Non-default interface method + if (enclosing.asClassOrInterfaceDeclaration().isInterface() + && !callable.asMethodDeclaration().isDefault()) { + handled = true; + node.remove(); + } + // abstract method + if (callable.asMethodDeclaration().isAbstract()) { + handled = true; + node.remove(); + } + } + if (!handled) { + blockStmt.setStatements( + new NodeList<>(StaticJavaParser.parseStatement("throw new java.lang.Error();"))); + } + } + // If an initializer is being removed, it's a field declaration to be trimmed + // Only replace with default value if it's a final field + else if (node instanceof Expression initializer + && node.getParentNode().get() instanceof VariableDeclarator fieldDeclarator + && fieldDeclarator.getInitializer().isPresent() + && fieldDeclarator.getInitializer().get().equals(initializer) + && fieldDeclarator.getParentNode().get() instanceof FieldDeclaration fieldDecl + && fieldDecl.isFinal()) { + fieldDeclarator.setInitializer( + JavaParserUtil.getInitializerRHS(fieldDeclarator.getType().toString())); + } else { + node.remove(); + } + } + + /** + * Represents the result of a slice. + * + * @param solvedSlice A set of compilation units, with all unused nodes trimmed, representing the + * slice of solved elements. + * @param generatedSymbolSlice A set of all generated symbols that must be included in the final + * output. + */ + public record SliceResult( + Set solvedSlice, + Set> generatedSymbolSlice, + Set generatedSymbolDependentSlice) { + // Override getter methods so we can add javadoc + + /** + * Gets each used compilation unit with all its unused nodes removed; does not include generated + * symbols. + * + * @return The result of the slice + */ + @Override + public Set solvedSlice() { + return solvedSlice; + } + + /** + * Gets all generated symbols that must be included in the final output. + * + * @return A set of unsolved symbols generated during the slice. + */ + @Override + public Set> generatedSymbolSlice() { + return generatedSymbolSlice; + } + + /** + * Gets all nodes that are dependent on at least one alternate and not part of the "mandatory" + * slice. + * + * @return All nodes that are only dependent on an alternate. + */ + @Override + public Set generatedSymbolDependentSlice() { + return generatedSymbolDependentSlice; + } + } +} diff --git a/src/main/java/org/checkerframework/specimin/SolveMethodOverridingVisitor.java b/src/main/java/org/checkerframework/specimin/SolveMethodOverridingVisitor.java deleted file mode 100644 index 75aba9a3d..000000000 --- a/src/main/java/org/checkerframework/specimin/SolveMethodOverridingVisitor.java +++ /dev/null @@ -1,53 +0,0 @@ -package org.checkerframework.specimin; - -import com.github.javaparser.ast.body.MethodDeclaration; -import com.github.javaparser.ast.visitor.Visitable; -import com.github.javaparser.resolution.UnsolvedSymbolException; -import com.github.javaparser.resolution.declarations.ResolvedMethodDeclaration; - -/** - * If used or target methods override another methods, this visitor updates the list of used classes - * and methods accordingly. - */ -public class SolveMethodOverridingVisitor extends SpeciminStateVisitor { - - /** - * Constructs a new SolveMethodOverridingVisitor with the provided sets of target methods, used - * members, and used classes. - * - * @param previousVisitor the last visitor to run before this one - */ - public SolveMethodOverridingVisitor(SpeciminStateVisitor previousVisitor) { - super(previousVisitor); - } - - @Override - public Visitable visit(MethodDeclaration method, Void p) { - String methodSignature; - try { - methodSignature = method.resolve().getQualifiedSignature(); - } catch (UnsolvedSymbolException | UnsupportedOperationException e) { - // this method is not used by target methods, so it is unresolved. - return super.visit(method, p); - } - if (targetMethods.contains(methodSignature) || usedMembers.contains(methodSignature)) { - checkForOverridingAndUpdateUsedClasses(method); - } - return super.visit(method, p); - } - - /** - * Given a MethodDeclaration, this method checks whether that method declaration overrides another - * method. If it does, it updates the list of used classes and members accordingly. Note: - * methodDeclaration is assumed to be a target or used method. - */ - private void checkForOverridingAndUpdateUsedClasses(MethodDeclaration methodDeclaration) { - ResolvedMethodDeclaration resolvedSuperCall = - MustImplementMethodsVisitor.getOverriddenMethodInSuperClass(methodDeclaration); - if (resolvedSuperCall != null) { - usedTypeElements.add( - resolvedSuperCall.getPackageName() + "." + resolvedSuperCall.getClassName()); - usedMembers.add(resolvedSuperCall.getQualifiedSignature()); - } - } -} diff --git a/src/main/java/org/checkerframework/specimin/SpeciminRunner.java b/src/main/java/org/checkerframework/specimin/SpeciminRunner.java index be5fdce2a..4a37d0f40 100644 --- a/src/main/java/org/checkerframework/specimin/SpeciminRunner.java +++ b/src/main/java/org/checkerframework/specimin/SpeciminRunner.java @@ -1,6 +1,5 @@ package org.checkerframework.specimin; -import com.github.javaparser.ParseProblemException; import com.github.javaparser.ParseResult; import com.github.javaparser.ParserConfiguration; import com.github.javaparser.StaticJavaParser; @@ -8,14 +7,15 @@ import com.github.javaparser.ast.ImportDeclaration; import com.github.javaparser.ast.Node; import com.github.javaparser.ast.PackageDeclaration; -import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration; -import com.github.javaparser.ast.body.EnumDeclaration; +import com.github.javaparser.ast.body.TypeDeclaration; import com.github.javaparser.ast.comments.Comment; import com.github.javaparser.symbolsolver.JavaSymbolSolver; import com.github.javaparser.symbolsolver.resolution.typesolvers.CombinedTypeSolver; import com.github.javaparser.symbolsolver.resolution.typesolvers.JarTypeSolver; import com.github.javaparser.symbolsolver.resolution.typesolvers.JavaParserTypeSolver; import com.github.javaparser.utils.SourceRoot; +import com.google.googlejavaformat.java.Formatter; +import com.google.googlejavaformat.java.FormatterException; import java.io.File; import java.io.IOException; import java.io.PrintWriter; @@ -23,9 +23,12 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.ArrayDeque; import java.util.ArrayList; +import java.util.Deque; import java.util.HashMap; import java.util.HashSet; +import java.util.IdentityHashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; @@ -36,9 +39,11 @@ import joptsimple.OptionSet; import joptsimple.OptionSpec; import org.apache.commons.io.FileUtils; -import org.checkerframework.checker.signature.qual.ClassGetSimpleName; -import org.checkerframework.checker.signature.qual.FullyQualifiedName; +import org.checkerframework.specimin.Slicer.SliceResult; import org.checkerframework.specimin.modularity.ModularityModel; +import org.checkerframework.specimin.unsolved.UnsolvedSymbolEnumerator; +import org.checkerframework.specimin.unsolved.UnsolvedSymbolEnumeratorResult; +import org.checkerframework.specimin.unsolved.UnsolvedSymbolGenerator; import org.jetbrains.java.decompiler.main.decompiler.ConsoleDecompiler; /** This class is the main runner for Specimin. Use its main() method to start Specimin. */ @@ -70,16 +75,23 @@ public static void main(String... args) throws IOException { // class.fully.qualified.Name#fieldName OptionSpec targetFieldsOptions = optionParser.accepts("targetField").withRequiredArg(); - // This option is to specify the modularity model. By default, the modularity model is + // The directory in which to output the results. + OptionSpec outputDirectoryOption = + optionParser.accepts("outputDirectory").withRequiredArg(); + + // This option determines how ambiguities are to be resolved. + // Accepts the arguments: "best-effort", "all", "input-condition" + OptionSpec ambiguityResolutionPolicy = + optionParser + .accepts("ambiguityResolutionPolicy") + .withOptionalArg() + .defaultsTo("best-effort"); + // the model for the javac type system, which is shared by the Checker Framework. // Accepts the arguments: "javac", "cf", "nullaway" OptionSpec modularityModelOption = optionParser.accepts("modularityModel").withOptionalArg().defaultsTo("cf"); - // The directory in which to output the results. - OptionSpec outputDirectoryOption = - optionParser.accepts("outputDirectory").withRequiredArg(); - OptionSet options = optionParser.parse(args); String jarDirectory = options.valueOf(jar); @@ -95,6 +107,7 @@ public static void main(String... args) throws IOException { options.valuesOf(targetMethodsOption), options.valuesOf(targetFieldsOptions), options.valueOf(outputDirectoryOption), + options.valueOf(ambiguityResolutionPolicy), options.valueOf(modularityModelOption)); } @@ -120,7 +133,14 @@ public static void performMinimization( String outputDirectory) throws IOException { performMinimization( - root, targetFiles, jarPaths, targetMethodNames, targetFieldNames, outputDirectory, "cf"); + root, + targetFiles, + jarPaths, + targetMethodNames, + targetFieldNames, + outputDirectory, + "best-effort", + "cf"); } /** @@ -134,7 +154,8 @@ public static void performMinimization( * @param targetMethodNames A set of target method names to be preserved. * @param targetFieldNames A set of target field names to be preserved. * @param outputDirectory The directory for the output. - * @param modularityModelCode the modularity model to use + * @param ambiguityResolutionPolicy The ambiguity resolution policy to use. + * @param modularityModelCode The modularity model to use. * @throws IOException if there is an exception */ public static void performMinimization( @@ -144,6 +165,7 @@ public static void performMinimization( List targetMethodNames, List targetFieldNames, String outputDirectory, + String ambiguityResolutionPolicy, String modularityModelCode) throws IOException { // The set of path of files that have been created by Specimin. We must be careful to delete all @@ -159,6 +181,7 @@ public void run() { } }); + AmbiguityResolutionPolicy policy = AmbiguityResolutionPolicy.parse(ambiguityResolutionPolicy); ModularityModel model = ModularityModel.createModularityModel(modularityModelCode); performMinimizationImpl( @@ -168,6 +191,7 @@ public void run() { targetMethodNames, targetFieldNames, outputDirectory, + policy, model, createdClass); } @@ -183,9 +207,11 @@ public void run() { * @param targetMethodNames A set of target method names to be preserved. * @param targetFieldNames A set of target field names to be preserved. * @param outputDirectory The directory for the output. - * @param modularityModel the modularity model + * @param ambiguityResolutionPolicy The ambiguity resolution policy. + * @param modularityModel The modularity model. * @throws IOException if there is an exception */ + @SuppressWarnings("UnusedVariable") // Remove once ambiguityResolutionPolicy is used private static void performMinimizationImpl( String root, List targetFiles, @@ -193,6 +219,7 @@ private static void performMinimizationImpl( List targetMethodNames, List targetFieldNames, String outputDirectory, + AmbiguityResolutionPolicy ambiguityResolutionPolicy, ModularityModel modularityModel, Set createdClass) throws IOException { @@ -202,217 +229,53 @@ private static void performMinimizationImpl( root = root + "/"; } - updateStaticSolver(root, jarPaths); - - // Keys are paths to files, values are parsed ASTs - Map parsedTargetFiles = new HashMap<>(); - for (String targetFile : targetFiles) { - parsedTargetFiles.put(targetFile, parseJavaFile(root, targetFile)); + if (!Path.of(root).isAbsolute()) { + root = Paths.get(root).toAbsolutePath().normalize().toString(); } - if (!jarPaths.isEmpty()) { - List argsToDecompile = new ArrayList<>(); - argsToDecompile.add("--silent"); - argsToDecompile.addAll(jarPaths); - argsToDecompile.add(root); - ConsoleDecompiler.main(argsToDecompile.toArray(new String[0])); - // delete unneccessary legal files - try { - FileUtils.deleteDirectory(new File(root + "META-INF")); - } catch (IOException ex) { - // Following decompilation, Windows raises an IOException because the files are still - // being used (by what?), so we should defer deletion until the end - for (File legalFile : - FileUtils.listFiles(new File(root + "META-INF"), new String[] {}, true)) { - createdClass.add(legalFile.toPath()); - } - } - } + ParserConfiguration config = updateStaticSolver(root, jarPaths); + decompileJarFiles(root, jarPaths, createdClass); - // the set of Java classes in the original codebase mapped with their corresponding Java files. - Map existingClassesToFilePath = new HashMap<>(); - // This map connects the fully-qualified names of non-primary classes with the fully-qualified - // names of their corresponding primary classes. A primary - // class is a class that has the same name as the Java file where the class is declared. - Map nonPrimaryClassesToPrimaryClass = new HashMap<>(); SourceRoot sourceRoot = new SourceRoot(Path.of(root)); + sourceRoot.setParserConfiguration(config); sourceRoot.tryToParse(); + + // the set of Java classes in the original codebase mapped with their corresponding Java files. + Map existingClassesToFilePath = new HashMap<>(); + Map fqnToCompilationUnits = new HashMap<>(); + + // Keys are paths to files, values are parsed ASTs + Map parsedTargetFiles = new HashMap<>(); + // getCompilationUnits does not seem to include all files, causing some to be deleted for (ParseResult res : sourceRoot.getCache()) { CompilationUnit compilationUnit = res.getResult().orElseThrow(() -> new RuntimeException(res.getProblems().toString())); Path pathOfCurrentJavaFile = compilationUnit.getStorage().get().getPath().toAbsolutePath().normalize(); - String primaryTypeQualifiedName = ""; - if (compilationUnit.getPrimaryType().isPresent()) { - // the get() is safe because primary type here is definitely not a local declaration, - // which does not have a fully-qualified name. - primaryTypeQualifiedName = - compilationUnit.getPrimaryType().get().getFullyQualifiedName().get(); - } - for (ClassOrInterfaceDeclaration declaredClass : - compilationUnit.findAll(ClassOrInterfaceDeclaration.class)) { - if (declaredClass.getFullyQualifiedName().isPresent()) { - String declaredClassQualifiedName = - declaredClass.getFullyQualifiedName().get().toString(); - existingClassesToFilePath.put(declaredClassQualifiedName, pathOfCurrentJavaFile); - // which means this class is not a primary class, and there is a primary class. - if (!"".equals(primaryTypeQualifiedName) - && !declaredClassQualifiedName.equals(primaryTypeQualifiedName)) { - nonPrimaryClassesToPrimaryClass.put( - declaredClassQualifiedName, primaryTypeQualifiedName); - } - } - } - for (EnumDeclaration enumDeclaration : compilationUnit.findAll(EnumDeclaration.class)) { - existingClassesToFilePath.put( - enumDeclaration.getFullyQualifiedName().get(), pathOfCurrentJavaFile); - } - } - UnsolvedSymbolVisitor addMissingClass = - new UnsolvedSymbolVisitor( - root, - existingClassesToFilePath, - new HashSet<>(targetMethodNames), - new HashSet<>(targetFieldNames), - modularityModel); - addMissingClass.setClassesFromJar(jarPaths); - - Map typesToChange = new HashMap<>(); - Map classAndUnresolvedInterface = new HashMap<>(); - Map methodRefToCorrectParameters = new HashMap<>(); - Map methodRefToVoidness = new HashMap<>(); - - // This is a defense against infinite loop bugs. The idea is this: - // if we encounter the same set of outputs three times, that's a good indication - // that we're in an infinite loop. But, we sometimes encounter the same set - // of outputs *twice* during normal operation (because some symbol needs to be - // solved). So, we track all previous iterations, and if we ever see the same - // outputs we set "problematicIteration" to that one. If we see that output again, - // we break the loop below early. - Set previousIterations = new HashSet<>(); - UnsolvedSymbolVisitorProgress problematicIteration = null; - - while (addMissingClass.gettingException()) { - addMissingClass.setExceptionToFalse(); - for (CompilationUnit cu : parsedTargetFiles.values()) { - addMissingClass.setImportStatement(cu.getImports()); - // it's important to make sure that getDeclarations and addMissingClass will visit the same - // file for each execution of the loop - FieldDeclarationsVisitor getDeclarations = new FieldDeclarationsVisitor(); - cu.accept(getDeclarations, null); - addMissingClass.setFieldNameToClassNameMap(getDeclarations.getFieldAndItsClass()); - cu.accept(addMissingClass, null); - } - addMissingClass.updateSyntheticSourceCode(); - createdClass.addAll(addMissingClass.getCreatedClass()); - // since the root directory is updated, we need to update the SymbolSolver - updateStaticSolver(root, jarPaths); - parsedTargetFiles = new HashMap<>(); + for (String targetFile : targetFiles) { - parsedTargetFiles.put(targetFile, parseJavaFile(root, targetFile)); - } - for (String targetFile : addMissingClass.getAddedTargetFiles()) { - try { - parsedTargetFiles.put(targetFile, parseJavaFile(root, targetFile)); - } catch (ParseProblemException e) { - // These parsing codes cause crashes in the CI. Those crashes can't be reproduced locally. - // Not sure if something is wrong with VineFlower or Specimin CI. Hence we keep these - // lines as tech debt. - // TODO: Figure out why the CI is crashing. - continue; - } - } - UnsolvedSymbolVisitorProgress workDoneAfterIteration = - new UnsolvedSymbolVisitorProgress( - addMissingClass.getPotentialUsedMembers(), - addMissingClass.getAddedTargetFiles(), - addMissingClass.getSyntheticClassesAsAStringSet()); - - // Infinite loop protection. - boolean gettingStuck = previousIterations.contains(workDoneAfterIteration); - if (gettingStuck) { - if (problematicIteration == null) { - problematicIteration = workDoneAfterIteration; - } else if (workDoneAfterIteration.equals(problematicIteration)) { - // This is the third time that we've made no changes, so we're probably - // in an infinite loop. - break; - } - } else { // not getting stuck - if (problematicIteration != null && !problematicIteration.equals(workDoneAfterIteration)) { - // unset problematicIteration - problematicIteration = null; + if (Path.of(root, targetFile).equals(pathOfCurrentJavaFile)) { + parsedTargetFiles.put(targetFile.replace('\\', '/'), compilationUnit); } } - previousIterations.add(workDoneAfterIteration); - - if (gettingStuck || !addMissingClass.gettingException()) { - // Three possible cases here: - // 1: addMissingClass has finished its iteration. - // 2: addMissingClass is stuck for some unknown reasons. - // 3: addMissingClass is stuck due to type mismatches, in which the JavaTypeCorrect call - // below should solve it. In this case (only), we should trigger another round - // of iteration of the unsolved symbol visitor, since JavaTypeCorrect may have caused - // some new symbols to be unsolved. - - // update the synthetic types by using error messages from javac. - GetTypesFullNameVisitor getTypesFullNameVisitor = new GetTypesFullNameVisitor(); - for (CompilationUnit cu : parsedTargetFiles.values()) { - cu.accept(getTypesFullNameVisitor, null); - } - Map> filesAndAssociatedTypes = - getTypesFullNameVisitor.getFileAndAssociatedTypes(); - // correct the types of all related files before adding them to parsedTargetFiles - JavaTypeCorrect typeCorrecter = - new JavaTypeCorrect(root, new HashSet<>(targetFiles), filesAndAssociatedTypes); - typeCorrecter.correctTypesForAllFiles(); - typesToChange = typeCorrecter.getTypeToChange(); - classAndUnresolvedInterface = typeCorrecter.getClassAndUnresolvedInterface(); - methodRefToCorrectParameters = typeCorrecter.getMethodRefToCorrectParameters(); - methodRefToVoidness = typeCorrecter.getMethodRefVoidness(); - boolean changeAtLeastOneType = addMissingClass.updateTypes(typesToChange); - boolean extendAtLeastOneType = - addMissingClass.updateTypesWithExtends(typeCorrecter.getExtendedTypes()); - boolean changeAtLeastOneMethodRef = - addMissingClass.updateMethodReferenceParameters(methodRefToCorrectParameters); - boolean changeAtLeastOneMethodReturn = - addMissingClass.updateMethodReferenceVoidness(methodRefToVoidness); - boolean atLeastOneTypeIsUpdated = - changeAtLeastOneType - || extendAtLeastOneType - || changeAtLeastOneMethodRef - || changeAtLeastOneMethodReturn; - - // this is case 2. We will stop addMissingClass. In the next phase, - // TargetMethodFinderVisitor will give us a meaningful exception message regarding which - // element in the input is not solvable. - if (!atLeastOneTypeIsUpdated && gettingStuck) { - break; - } else if (atLeastOneTypeIsUpdated) { - // this is case 3: ensure that unsolved symbol solver is called at least once, to force us - // to reach a correct fixpoint - addMissingClass.gotException(); - continue; - } - // in order for the newly updated files to be considered when solving symbols, we need to - // update the type solver and the map of parsed target files. - updateStaticSolver(root, jarPaths); + for (TypeDeclaration declaredClass : compilationUnit.findAll(TypeDeclaration.class)) { + if (declaredClass.getFullyQualifiedName().isPresent()) { + String declaredClassQualifiedName = declaredClass.getFullyQualifiedName().get(); + existingClassesToFilePath.put(declaredClassQualifiedName, pathOfCurrentJavaFile); + fqnToCompilationUnits.put(declaredClassQualifiedName, compilationUnit); + } } } - EnumVisitor enumVisitor = new EnumVisitor(addMissingClass); - for (CompilationUnit cu : parsedTargetFiles.values()) { - cu.accept(enumVisitor, null); - } + createdClass.addAll(getPathsFromJarPaths(root, jarPaths)); - // Use a two-phase approach: the first phase finds the target(s) and records - // what specifications they use, and the second phase takes that information - // and removes all non-used code. + Deque worklist = new ArrayDeque<>(); TargetMemberFinderVisitor finder = - new TargetMemberFinderVisitor(enumVisitor, nonPrimaryClassesToPrimaryClass); + new TargetMemberFinderVisitor( + targetMethodNames, targetFieldNames, worklist, modularityModel); for (CompilationUnit cu : parsedTargetFiles.values()) { cu.accept(finder, null); @@ -432,130 +295,157 @@ private static void performMinimizationImpl( + unfoundMembersTable(unfoundFields, false)); } - SolveMethodOverridingVisitor solveMethodOverridingVisitor = - new SolveMethodOverridingVisitor(finder); - for (CompilationUnit cu : parsedTargetFiles.values()) { - cu.accept(solveMethodOverridingVisitor, null); + UnsolvedSymbolGenerator unsolvedSymbolGenerator = + new UnsolvedSymbolGenerator(fqnToCompilationUnits); + SliceResult sliceResult = + Slicer.slice( + new StandardTypeRuleDependencyMap(fqnToCompilationUnits), + worklist, + unsolvedSymbolGenerator, + fqnToCompilationUnits); + + // cache to avoid called Files.createDirectories repeatedly with the same arguments + Set createdDirectories = new HashSet<>(); + Set targetFilesAbsolutePaths = new HashSet<>(); + + for (String target : targetFiles) { + File targetFile = new File(target); + // Convert to absolute path for comparison + targetFilesAbsolutePaths.add(targetFile.getAbsolutePath()); } - Set relatedClass = new HashSet<>(parsedTargetFiles.keySet()); - // add all files related to the targeted methods - for (String classFullName : solveMethodOverridingVisitor.getUsedTypeElements()) { - String directoryOfFile = classFullName.replace(".", "/") + ".java"; - File thisFile = new File(root + directoryOfFile); - // classes from JDK are automatically on the classpath, so UnsolvedSymbolVisitor will not - // create synthetic files for them - if (thisFile.exists()) { - relatedClass.add(directoryOfFile); + UnsolvedSymbolEnumerator alternateOutput = + new UnsolvedSymbolEnumerator(sliceResult.generatedSymbolSlice()); + UnsolvedSymbolEnumeratorResult enumeratorResult = + alternateOutput.getBestEffort(sliceResult.generatedSymbolDependentSlice()); + Formatter formatter = new Formatter(); + + handleUnsolvedSymbolEnumeratorResult( + sliceResult, + enumeratorResult, + existingClassesToFilePath, + root, + targetFilesAbsolutePaths, + outputDirectory, + createdDirectories, + formatter); + } + + /** + * Handles a result from an iteration of {@link UnsolvedSymbolEnumerator}. This outputs the files + * for both solved and unsolved symbols. + * + * @param sliceResult The result of the slice + * @param enumeratorResult The iteration of the UnsolvedSymbolEnumerator + * @param existingClassesToFilePath A map of existing classes to their files paths + * @param root The root directory + * @param targetFilesAbsolutePaths The target files as absolute paths + * @param outputDirectory The output directory + * @param createdDirectories A cache of created directories + * @param formatter A formatter for the output + */ + private static void handleUnsolvedSymbolEnumeratorResult( + SliceResult sliceResult, + UnsolvedSymbolEnumeratorResult enumeratorResult, + Map existingClassesToFilePath, + String root, + Set targetFilesAbsolutePaths, + String outputDirectory, + Set createdDirectories, + Formatter formatter) + throws IOException { + Set usedPackages = new HashSet<>(); + for (CompilationUnit cu : sliceResult.solvedSlice()) { + if (!cu.getPackageDeclaration().isPresent()) { + usedPackages.add(""); + continue; } + usedPackages.add(cu.getPackageDeclaration().get().getNameAsString()); } - for (String directory : relatedClass) { - // directories already in parsedTargetFiles are original files in the root directory, we are - // not supposed to update them. - if (!parsedTargetFiles.containsKey(directory)) { - try { - parsedTargetFiles.put(directory, parseJavaFile(root, directory)); - } catch (ParseProblemException e) { - // TODO: Figure out why the CI is crashing. - continue; - } + for (String className : enumeratorResult.classNamesToFileContent().keySet()) { + int lastDot = className.lastIndexOf('.'); + if (lastDot < 0) { + usedPackages.add(""); + } else { + usedPackages.add(className.substring(0, lastDot)); } } - Set classToFindInheritance = solveMethodOverridingVisitor.getUsedTypeElements(); - Set totalSetOfAddedInheritedClasses = classToFindInheritance; - InheritancePreserveVisitor inheritancePreserve; - while (!classToFindInheritance.isEmpty()) { - inheritancePreserve = new InheritancePreserveVisitor(classToFindInheritance); - for (CompilationUnit cu : parsedTargetFiles.values()) { - cu.accept(inheritancePreserve, null); + + for (CompilationUnit original : sliceResult.solvedSlice()) { + if (isEmptyCompilationUnit(original)) { + continue; } - for (String targetFile : inheritancePreserve.getAddedClasses()) { - String directoryOfFile = targetFile.replace(".", "/") + ".java"; - File thisFile = new File(root + directoryOfFile); - if (thisFile.exists()) { - try { - parsedTargetFiles.put(directoryOfFile, parseJavaFile(root, directoryOfFile)); - } catch (ParseProblemException e) { - // TODO: Figure out why the CI is crashing. - continue; - } + + // Generally, this set will be small, so we'll check if we need to clone at all + // to prevent the cloning process from happening when it's not necessary + boolean shouldClone = false; + for (Node node : enumeratorResult.unusedDependentNodes()) { + if (node.findCompilationUnit().get().equals(original)) { + shouldClone = true; + break; } } - classToFindInheritance = inheritancePreserve.getAddedClasses(); - totalSetOfAddedInheritedClasses.addAll(classToFindInheritance); - inheritancePreserve.emptyAddedClasses(); - } - - solveMethodOverridingVisitor.getUsedTypeElements().addAll(totalSetOfAddedInheritedClasses); - MustImplementMethodsVisitor mustImplementMethodsVisitor = - new MustImplementMethodsVisitor(solveMethodOverridingVisitor); + CompilationUnit cu = original; + if (shouldClone) { + cu = original.clone(); - for (CompilationUnit cu : parsedTargetFiles.values()) { - cu.accept(mustImplementMethodsVisitor, null); - } + IdentityHashMap map = new IdentityHashMap<>(); + mapNodes(original, cu, map); - // This is safe to run after MustImplementMethodsVisitor because - // annotations do not inherit - processAnnotationTypes(mustImplementMethodsVisitor, root, parsedTargetFiles); + for (Node toRemove : enumeratorResult.unusedDependentNodes()) { + Node clone = map.get(toRemove); - // Remove the unsolved annotations (and @Override) in all files. - UnsolvedAnnotationRemoverVisitor annoRemover = new UnsolvedAnnotationRemoverVisitor(jarPaths); - for (CompilationUnit cu : parsedTargetFiles.values()) { - cu.accept(annoRemover, null); - } - - PrunerVisitor methodPruner = - new PrunerVisitor( - mustImplementMethodsVisitor, - finder.getResolvedYetStuckMethodCall(), - classAndUnresolvedInterface); - - for (CompilationUnit cu : parsedTargetFiles.values()) { - cu.accept(methodPruner, null); - } - - pruneAnnotationDeclarationTargets(parsedTargetFiles); - removeUnusedImports(parsedTargetFiles); - - // cache to avoid called Files.createDirectories repeatedly with the same arguments - Set createdDirectories = new HashSet<>(); - Set targetFilesAbsolutePaths = new HashSet<>(); + if (clone == null) { + continue; + } + clone.remove(); + } + } - for (String target : targetFiles) { - File targetFile = new File(target); - // Convert to absolute path for comparison - targetFilesAbsolutePaths.add(targetFile.getAbsolutePath()); - } + String path = + qualifiedNameToFilePath( + cu.getPrimaryType().get().getFullyQualifiedName().get(), + existingClassesToFilePath, + root); - for (Entry target : parsedTargetFiles.entrySet()) { // ignore classes from the Java package, unless we are targeting a JDK file. // However, all related java/ files should not be included (as in used, but not targeted) - String absolutePath = new File(target.getKey()).getAbsolutePath(); + String absolutePath = new File(path).getAbsolutePath(); if (!targetFilesAbsolutePaths.contains(absolutePath) - && (target.getKey().startsWith("java/") || target.getKey().startsWith("java\\"))) { + && (path.startsWith("java/") || path.startsWith("java\\"))) { continue; } - // If a compilation output's entire body has been removed and the related class is not used by - // the target methods, do not output it. - if (isEmptyCompilationUnit(target.getValue())) { - // target key will have this form: "path/of/package/ClassName.java" - String classFullyQualifiedName = getFullyQualifiedClassName(target.getKey()); - @SuppressWarnings("signature") // since it's the last element of a fully qualified path - @ClassGetSimpleName String simpleName = - classFullyQualifiedName.substring(classFullyQualifiedName.lastIndexOf(".") + 1); - // If this condition is true, this class is a synthetic class initially created to be a - // return type of some synthetic methods, but later javac has found the correct return type - // for that method. - if (typesToChange.containsKey(simpleName)) { - continue; - } - if (!finder.getUsedTypeElements().contains(classFullyQualifiedName)) { - continue; - } + Path targetOutputPath = Path.of(outputDirectory, path); + // Create any parts of the directory structure that don't already exist. + Path dirContainingOutputFile = targetOutputPath.getParent(); + // This null test is very defensive and might not be required? I think getParent can + // only return null if its input was a single element path, which targetOutputPath + // should not be unless the user made an error. + if (dirContainingOutputFile != null + && !createdDirectories.contains(dirContainingOutputFile)) { + Files.createDirectories(dirContainingOutputFile); + createdDirectories.add(dirContainingOutputFile); } - Path targetOutputPath = Path.of(outputDirectory, target.getKey()); + // Write the string representation of CompilationUnit to the file + try (PrintWriter writer = + new PrintWriter(targetOutputPath.toFile(), StandardCharsets.UTF_8)) { + writer.print( + formatter.formatSourceAndFixImports( + getCompilationUnitWithUnusedWildcardImportsRemoved( + getCompilationUnitWithCommentsTrimmed(cu), usedPackages) + .toString())); + } catch (IOException | FormatterException e) { + System.out.println("failed to write output file " + targetOutputPath); + System.out.println("with error: " + e); + } + } + + // Generated files do not have imports, so we don't need to call the formatter. + for (Entry alternate : enumeratorResult.classNamesToFileContent().entrySet()) { + Path targetOutputPath = + Path.of(outputDirectory, alternate.getKey().replace('.', '/') + ".java"); // Create any parts of the directory structure that don't already exist. Path dirContainingOutputFile = targetOutputPath.getParent(); // This null test is very defensive and might not be required? I think getParent can @@ -569,125 +459,86 @@ private static void performMinimizationImpl( // Write the string representation of CompilationUnit to the file try { PrintWriter writer = new PrintWriter(targetOutputPath.toFile(), StandardCharsets.UTF_8); - writer.print(getCompilationUnitWithCommentsTrimmed(target.getValue())); + writer.print(alternate.getValue()); writer.close(); } catch (IOException e) { System.out.println("failed to write output file " + targetOutputPath); System.out.println("with error: " + e); } } - createdClass.addAll(getPathsFromJarPaths(root, jarPaths)); } /** - * Fully solve all annotations by processing all annotations, annotation parameters, and their - * types. This method also removes any annotations which are not fully solvable and includes all - * necessary files in Specimin's output. + * Creates a map of original nodes to cloned nodes. * - * @param last The last SpeciminStateVisitor to run - * @param root The root directory - * @param parsedTargetFiles A map of file names to parsed CompilationUnits + * @param original The original node + * @param clone The cloned node + * @param map The final mapping */ - private static SpeciminStateVisitor processAnnotationTypes( - SpeciminStateVisitor last, String root, Map parsedTargetFiles) - throws IOException { - AnnotationParameterTypesVisitor annotationParameterTypesVisitor = - new AnnotationParameterTypesVisitor(last); + private static void mapNodes(Node original, Node clone, IdentityHashMap map) { + map.put(original, clone); - Set classesToParse = new HashSet<>(); - Set compilationUnitsToSolveAnnotations = - new HashSet<>(parsedTargetFiles.values()); + List originalChildNodes = original.getChildNodes(); + List cloneChildNodes = clone.getChildNodes(); - while (!compilationUnitsToSolveAnnotations.isEmpty()) { - for (CompilationUnit cu : compilationUnitsToSolveAnnotations) { - cu.accept(annotationParameterTypesVisitor, null); - } - - // add all files related to the target annotations - for (String annoFullName : annotationParameterTypesVisitor.getClassesToAdd()) { - if (annotationParameterTypesVisitor.getUsedTypeElements().contains(annoFullName)) { - continue; - } - String directoryOfFile = annoFullName.replace(".", "/") + ".java"; - File thisFile = new File(root + directoryOfFile); - // classes from JDK are automatically on the classpath, so UnsolvedSymbolVisitor will not - // create synthetic files for them - if (thisFile.exists()) { - classesToParse.add(directoryOfFile); - } else { - // The given class may be an inner class, so we should find its encapsulating class - // Assuming following Java conventions, we will find the first instance of .{capital} - // and trim off subsequent .*s. - int dot = annoFullName.indexOf('.'); - while (dot != -1) { - if (Character.isUpperCase(annoFullName.charAt(dot + 1))) { - dot = annoFullName.indexOf('.', dot + 1); - break; - } - dot = annoFullName.indexOf('.', dot + 1); - } + for (int i = 0; i < originalChildNodes.size(); i++) { + mapNodes(originalChildNodes.get(i), cloneChildNodes.get(i), map); + } + } - if (dot != -1) { - directoryOfFile = annoFullName.substring(0, dot).replace(".", "/") + ".java"; - thisFile = new File(root + directoryOfFile); - // This inner class was just added, so we should re-parse the file - if (thisFile.exists()) { - classesToParse.add(directoryOfFile); - } - } - } + /** + * Removes all wildcard imports that are not used in the given set of package names. + * + * @param cu The CompilationUnit to process. + * @param usedPackages A set of package names that are used in the code. + * @return The modified CompilationUnit with unused wildcard imports removed. + */ + private static CompilationUnit getCompilationUnitWithUnusedWildcardImportsRemoved( + CompilationUnit cu, Set usedPackages) { + for (ImportDeclaration decl : List.copyOf(cu.getImports())) { + if (!decl.isAsterisk()) { + continue; } + String packageName = decl.getNameAsString(); - compilationUnitsToSolveAnnotations.clear(); - - for (String directory : classesToParse) { - // We need to continue solving annotations and parameters in newly added annotation files - try { - // directories already in parsedTargetFiles are original files in the root directory, we - // are not supposed to update them. - if (!parsedTargetFiles.containsKey(directory)) { - CompilationUnit parsed = parseJavaFile(root, directory); - parsedTargetFiles.put(directory, parsed); - } - compilationUnitsToSolveAnnotations.add(parsedTargetFiles.get(directory)); - } catch (ParseProblemException e) { - // TODO: Figure out why the CI is crashing. - continue; - } + if (JavaLangUtils.inJdkPackage(packageName)) { + continue; } - classesToParse.clear(); - - annotationParameterTypesVisitor - .getUsedTypeElements() - .addAll(annotationParameterTypesVisitor.getClassesToAdd()); - annotationParameterTypesVisitor.getClassesToAdd().clear(); - } - return annotationParameterTypesVisitor; - } - - /** Runs AnnotationTargetRemoverVisitor on the target files. Call after PrunerVisitor. */ - private static void pruneAnnotationDeclarationTargets( - Map parsedTargetFiles) { - AnnotationTargetRemoverVisitor targetPruner = new AnnotationTargetRemoverVisitor(); - for (CompilationUnit cu : parsedTargetFiles.values()) { - cu.accept(targetPruner, null); + if (!usedPackages.contains(packageName)) { + decl.remove(); + } } - - targetPruner.removeExtraAnnotationTargets(); + return cu; } /** - * Removes all unused imports in each output file through {@code UnusedImportRemoverVisitor}. + * Decompiles the given jar files into the specified root directory. * - * @param parsedTargetFiles the files to remove unused imports + * @param root The root directory where the jar files will be decompiled. + * @param jarPaths The list of paths to the jar files to be decompiled. + * @param createdClass A set to keep track of all created class files during decompilation. */ - private static void removeUnusedImports(Map parsedTargetFiles) { - UnusedImportRemoverVisitor unusedImportRemover = new UnusedImportRemoverVisitor(); + private static void decompileJarFiles( + String root, List jarPaths, Set createdClass) { + if (!jarPaths.isEmpty()) { + List argsToDecompile = new ArrayList<>(); + argsToDecompile.add("--silent"); + argsToDecompile.addAll(jarPaths); + argsToDecompile.add(root); + ConsoleDecompiler.main(argsToDecompile.toArray(new String[0])); + // delete unneccessary legal files + try { + FileUtils.deleteDirectory(new File(root + "META-INF")); + } catch (IOException ex) { + // Following decompilation, Windows raises an IOException because the files are still + // being used (by what?), so we should defer deletion until the end - for (CompilationUnit cu : parsedTargetFiles.values()) { - cu.accept(unusedImportRemover, null); - unusedImportRemover.removeUnusedImports(); + for (File legalFile : + FileUtils.listFiles(new File(root + "META-INF"), new String[] {}, true)) { + createdClass.add(legalFile.toPath()); + } + } } } @@ -726,35 +577,28 @@ private static String unfoundMembersTable( * @param jarPaths the list of jar files to be used as input. * @throws IOException if something went wrong. */ - private static void updateStaticSolver(String root, List jarPaths) throws IOException { + private static ParserConfiguration updateStaticSolver(String root, List jarPaths) + throws IOException { // Set up the parser's symbol solver, so that we can resolve definitions. CombinedTypeSolver typeSolver = new CombinedTypeSolver(new JdkTypeSolver(), new JavaParserTypeSolver(new File(root))); + for (String path : jarPaths) { typeSolver.add(new JarTypeSolver(path)); } + + JavaParserUtil.setTypeSolver(typeSolver); + JavaSymbolSolver symbolSolver = new JavaSymbolSolver(typeSolver); - StaticJavaParser.getParserConfiguration().setSymbolResolver(symbolSolver); - StaticJavaParser.getParserConfiguration() - .setLanguageLevel(ParserConfiguration.LanguageLevel.JAVA_17); - } - /** - * Converts a path to a Java file into the fully-qualified name of the public class in that file, - * relying on the file's relative path being the same as the package name. - * - * @param javaFilePath the path to a .java file, in this form: "path/of/package/ClassName.java". - * Note that this path must be rooted at the same directory in which javac could be invoked to - * compile the file - * @return the fully-qualified name of the given class - */ - @SuppressWarnings("signature") // string manipulation - private static @FullyQualifiedName String getFullyQualifiedClassName(final String javaFilePath) { - String result = javaFilePath.replace("/", "."); - if (!result.endsWith(".java")) { - throw new RuntimeException("A Java file path does not end with .java: " + result); - } - return result.substring(0, result.length() - 5); + ParserConfiguration config = + new ParserConfiguration() + .setSymbolResolver(symbolSolver) + .setLanguageLevel(ParserConfiguration.LanguageLevel.JAVA_17); + + StaticJavaParser.setConfiguration(config); + + return config; } /** @@ -772,12 +616,6 @@ private static boolean isEmptyCompilationUnit(CompilationUnit cu) { || child instanceof Comment) { // Package declarations, imports, and comments don't count for the purposes of // deciding whether to entirely remove a compilation unit. - } else if (child instanceof ClassOrInterfaceDeclaration) { - ClassOrInterfaceDeclaration cdecl = - ((ClassOrInterfaceDeclaration) child).asClassOrInterfaceDeclaration(); - if (!cdecl.getMembers().isEmpty()) { - return false; - } } else { return false; } @@ -785,34 +623,22 @@ private static boolean isEmptyCompilationUnit(CompilationUnit cu) { return true; } - /** - * Use JavaParser to parse a single Java files. - * - * @param root the absolute path to the root of the source tree - * @param path the path of the file to be parsed, relative to the root - * @return the compilation unit representing the code in the file at the path, or exit with an - * error - */ - private static CompilationUnit parseJavaFile(String root, String path) throws IOException { - return StaticJavaParser.parse(Path.of(root, path)); - } - /** * Retrieves the paths of Java files that should be created from the list of JAR files. * - * @param outPutDirectory The directory where the Java files will be created. + * @param outputDirectory The directory where the Java files will be created. * @param jarPaths The set of paths to JAR files. * @return A set containing the paths of the Java files to be created. * @throws IOException If an I/O error occurs. */ - private static Set getPathsFromJarPaths(String outPutDirectory, List jarPaths) + private static Set getPathsFromJarPaths(String outputDirectory, List jarPaths) throws IOException { Set pathsOfFile = new HashSet<>(); for (String path : jarPaths) { JarTypeSolver jarSolver = new JarTypeSolver(path); for (String qualifedClassName : jarSolver.getKnownClasses()) { String relativePath = qualifedClassName.replace(".", "/") + ".java"; - String absolutePath = outPutDirectory + relativePath; + String absolutePath = outputDirectory + relativePath; Path filePath = Paths.get(absolutePath); if (Files.exists(filePath)) { pathsOfFile.add(filePath); @@ -847,7 +673,7 @@ private static void deleteFiles(Set fileList) { } } } catch (Exception e) { - throw new RuntimeException("Unresolved file path: " + filePath); + throw new RuntimeException("Unresolved file path: " + filePath, e); } } } @@ -904,4 +730,25 @@ private static List getJarFiles(String directoryPath) throws IOException throw new RuntimeException(e); } } + + /** + * Gets the path of the file containing the definition for the class represented by a qualified + * name. Throws an exception if this class is not in the original directory. + * + * @param qualifiedName The qualified name of the type + * @param existingClassesToFilePath The map of existing classes to file paths + * @param rootDirectory The root directory + * @return The relative path of the file containing the definition of the class + */ + private static String qualifiedNameToFilePath( + String qualifiedName, Map existingClassesToFilePath, String rootDirectory) { + if (!existingClassesToFilePath.containsKey(qualifiedName)) { + throw new RuntimeException( + "qualifiedNameToFilePath only works for classes in the original directory"); + } + Path absoluteFilePath = existingClassesToFilePath.get(qualifiedName); + // theoretically rootDirectory should already be absolute as stated in README. + Path absoluteRootDirectory = Paths.get(rootDirectory).toAbsolutePath(); + return absoluteRootDirectory.relativize(absoluteFilePath).toString().replace('\\', '/'); + } } diff --git a/src/main/java/org/checkerframework/specimin/SpeciminStateVisitor.java b/src/main/java/org/checkerframework/specimin/SpeciminStateVisitor.java deleted file mode 100644 index 00e5070aa..000000000 --- a/src/main/java/org/checkerframework/specimin/SpeciminStateVisitor.java +++ /dev/null @@ -1,342 +0,0 @@ -package org.checkerframework.specimin; - -import com.github.javaparser.ast.Node; -import com.github.javaparser.ast.body.AnnotationDeclaration; -import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration; -import com.github.javaparser.ast.body.ConstructorDeclaration; -import com.github.javaparser.ast.body.EnumDeclaration; -import com.github.javaparser.ast.body.FieldDeclaration; -import com.github.javaparser.ast.body.MethodDeclaration; -import com.github.javaparser.ast.body.TypeDeclaration; -import com.github.javaparser.ast.body.VariableDeclarator; -import com.github.javaparser.ast.expr.SimpleName; -import com.github.javaparser.ast.nodeTypes.NodeWithDeclaration; -import com.github.javaparser.ast.visitor.ModifierVisitor; -import com.github.javaparser.ast.visitor.Visitable; -import com.github.javaparser.resolution.UnsolvedSymbolException; -import java.nio.file.Path; -import java.util.HashSet; -import java.util.Map; -import java.util.Optional; -import java.util.Set; -import org.checkerframework.checker.signature.qual.ClassGetSimpleName; -import org.checkerframework.specimin.modularity.ModularityModel; - -/** - * This visitor contains shared logic and state for the Specimin's various XVisitor classes. It - * should not be used directly. - * - *

This class tracks the following: the lists of target methods and fields, the lists of used - * members and classes, and the set of existing classes to file paths. It may be expanded to handle - * additional state tracking in the future. - */ -public abstract class SpeciminStateVisitor extends ModifierVisitor { - - /** The modularity model currently in use. */ - protected final ModularityModel modularityModel; - - /** - * Set containing the signatures of target methods. The Strings in the set are the fully-qualified - * names, as returned by ResolvedMethodDeclaration#getQualifiedSignature. - */ - protected final Set targetMethods; - - /** - * Set containing the fully-qualified names of target fields. The format is - * class.fully.qualified.Name#fieldName. - */ - protected final Set targetFields; - - /** - * The members (methods and fields) that were actually used by the targets, and therefore ought to - * have their specifications (but not bodies) preserved. The Strings in the set are the - * fully-qualified names, as returned by ResolvedMethodDeclaration#getQualifiedSignature for - * methods and FieldAccessExpr#getName for fields. - */ - protected final Set usedMembers; - - /** - * Type elements (classes, interfaces, and enums) related to the methods used by the targets. - * These classes will be included in the input. - */ - protected final Set usedTypeElements; - - /** for checking if class files are in the original codebase. */ - protected final Map existingClassesToFilePath; - - /** - * This boolean tracks whether the element currently being visited is inside a target method or - * field. - */ - protected boolean insideTargetMember = false; - - /** - * Is the visitor inside a target constructor? If this boolean is true, then {@link - * #insideTargetMember} is also guaranteed to be true. - */ - protected boolean insideTargetCtor = false; - - /** The simple name of the class currently visited */ - protected @ClassGetSimpleName String className = ""; - - /** The qualified name of the class currently being visited. */ - protected String currentClassQualifiedName = ""; - - /** - * The fully-qualified names of each field that is assigned by a target constructor. The - * assignments to these fields will be preserved, so Specimin needs to avoid adding an initializer - * for them if they are final (as it does for other, non-assigned-by-target final fields). Set by - * {@link TargetMemberFinderVisitor} but stored here so that it is easily available later when - * pruning. - */ - protected final Set fieldsAssignedByTargetCtors; - - /** - * Constructs a new instance with the provided sets. Use this constructor only for the first - * visitor to run. - * - * @param targetMethods the fully-qualified signatures of the target methods, in the form returned - * by ResolvedMethodDeclaration#getQualifiedSignature but optionally containing spaces between - * parameters, which this constructor guarantees will be removed - * @param targetFields the fully-qualified names of the target fields, in the form - * class.fully.qualified.Name#fieldName - * @param usedMembers set containing the signatures of used members - * @param usedTypeElements set containing the signatures of used classes, enums, annotations, etc. - * @param model the modularity model - * @param existingClassesToFilePath map from existing classes to file paths - */ - public SpeciminStateVisitor( - Set targetMethods, - Set targetFields, - Set usedMembers, - Set usedTypeElements, - ModularityModel model, - Map existingClassesToFilePath) { - this.targetMethods = new HashSet<>(); - for (String methodSignature : targetMethods) { - // remove spaces - this.targetMethods.add(methodSignature.replaceAll("\\s", "")); - } - this.targetFields = targetFields; - this.usedMembers = usedMembers; - this.usedTypeElements = usedTypeElements; - this.existingClassesToFilePath = existingClassesToFilePath; - this.fieldsAssignedByTargetCtors = new HashSet<>(); - this.modularityModel = model; - } - - /** - * Constructor that copies state from the previous visitor. All state remains mutable (it's a - * shallow copy). - * - * @param previous the previous visitor to run - */ - public SpeciminStateVisitor(SpeciminStateVisitor previous) { - this.targetMethods = previous.targetMethods; - this.targetFields = previous.targetFields; - this.usedTypeElements = previous.usedTypeElements; - this.usedMembers = previous.usedMembers; - this.existingClassesToFilePath = previous.existingClassesToFilePath; - this.insideTargetMember = previous.insideTargetMember; - this.className = previous.className; - this.currentClassQualifiedName = previous.currentClassQualifiedName; - this.fieldsAssignedByTargetCtors = previous.fieldsAssignedByTargetCtors; - this.modularityModel = previous.modularityModel; - } - - /** - * Get the set containing the signatures of used classes. - * - * @return The set containing the signatures of used classes. - */ - public Set getUsedTypeElements() { - return usedTypeElements; - } - - /** - * Gets the (fully-qualified) signature of a declaration (method or constructor). Removes things - * like annotations, the return type, spaces, etc. - * - * @param decl a method or constructor declaration - * @return the fully qualified signature of that declaration - */ - protected String getSignature(NodeWithDeclaration decl) { - StringBuilder result = new StringBuilder(); - result.append(this.currentClassQualifiedName); - result.append("#"); - result.append(JavaParserUtil.removeMethodReturnTypeSpacesAndAnnotations(decl)); - return result.toString(); - } - - @Override - public Visitable visit(VariableDeclarator var, Void p) { - boolean oldInsideTargetMember = insideTargetMember; - insideTargetMember = - oldInsideTargetMember - || targetFields.contains(currentClassQualifiedName + "#" + var.getNameAsString()); - Visitable result = super.visit(var, p); - insideTargetMember = oldInsideTargetMember; - return result; - } - - @Override - public Visitable visit(MethodDeclaration methodDeclaration, Void p) { - boolean oldInsideTargetMember = insideTargetMember; - String methodQualifiedSignature = getSignature(methodDeclaration); - insideTargetMember = oldInsideTargetMember || targetMethods.contains(methodQualifiedSignature); - Visitable result = super.visit(methodDeclaration, p); - insideTargetMember = oldInsideTargetMember; - return result; - } - - @Override - public Visitable visit(ConstructorDeclaration ctorDecl, Void p) { - String methodQualifiedSignature = getSignature(ctorDecl); - boolean oldInsideTargetMember = insideTargetMember; - insideTargetMember = oldInsideTargetMember || targetMethods.contains(methodQualifiedSignature); - boolean oldInsideTargetCtor = insideTargetCtor; - insideTargetCtor = oldInsideTargetCtor || targetMethods.contains(methodQualifiedSignature); - Visitable result = super.visit(ctorDecl, p); - insideTargetCtor = oldInsideTargetCtor; - insideTargetMember = oldInsideTargetMember; - return result; - } - - @Override - public Visitable visit(EnumDeclaration node, Void arg) { - maintainDataStructuresPreSuper(node); - Visitable result = super.visit(node, arg); - maintainDataStructuresPostSuper(node); - return result; - } - - @Override - public Visitable visit(ClassOrInterfaceDeclaration node, Void arg) { - maintainDataStructuresPreSuper(node); - Visitable result = super.visit(node, arg); - maintainDataStructuresPostSuper(node); - return result; - } - - /** - * Maintains the data structures of this class (like the {@link #className}, {@link - * #currentClassQualifiedName}, etc.) based on a class, interface, or enum declaration. Call this - * method before calling super.visit(). - * - * @param decl the class, interface, or enum declaration - */ - protected void maintainDataStructuresPreSuper(TypeDeclaration decl) { - SimpleName nodeName = decl.getName(); - className = nodeName.asString(); - if (decl.isNestedType()) { - this.currentClassQualifiedName += "." + decl.getName().asString(); - } else if (!JavaParserUtil.isLocalClassDecl(decl)) { - // the purpose of keeping track of class name is to recognize the signatures of target - // methods. Since we don't support methods inside local classes as target methods, we don't - // need - // to keep track of class name in this case. - this.currentClassQualifiedName = decl.getFullyQualifiedName().orElseThrow(); - } - } - - /** - * Maintains the data structures of this class (like the {@link #className}, {@link - * #currentClassQualifiedName}, etc.) based on a class, interface, or enum declaration. Call this - * method after calling super.visit(). - * - * @param decl the class, interface, or enum declaration - */ - protected void maintainDataStructuresPostSuper(TypeDeclaration decl) { - if (decl.isNestedType()) { - this.currentClassQualifiedName = - this.currentClassQualifiedName.substring( - 0, this.currentClassQualifiedName.lastIndexOf('.')); - } else if (!JavaParserUtil.isLocalClassDecl(decl)) { - this.currentClassQualifiedName = ""; - } - } - - /** - * Determines if the given Node is a target/used member or class. - * - * @param node The node to check - * @return true iff the given Node is a target/used member or type. - */ - protected boolean isTargetOrUsed(Node node) { - String qualifiedName; - boolean isClass = false; - if (node instanceof ClassOrInterfaceDeclaration) { - Optional qualifiedNameOptional = - ((ClassOrInterfaceDeclaration) node).getFullyQualifiedName(); - if (qualifiedNameOptional.isEmpty()) { - return false; - } - qualifiedName = qualifiedNameOptional.get(); - isClass = true; - } else if (node instanceof EnumDeclaration) { - Optional qualifiedNameOptional = ((EnumDeclaration) node).getFullyQualifiedName(); - if (qualifiedNameOptional.isEmpty()) { - return false; - } - qualifiedName = qualifiedNameOptional.get(); - isClass = true; - } else if (node instanceof AnnotationDeclaration) { - Optional qualifiedNameOptional = - ((AnnotationDeclaration) node).getFullyQualifiedName(); - if (qualifiedNameOptional.isEmpty()) { - return false; - } - qualifiedName = qualifiedNameOptional.get(); - isClass = true; - } else if (node instanceof ConstructorDeclaration) { - try { - qualifiedName = ((ConstructorDeclaration) node).resolve().getQualifiedSignature(); - } catch (UnsolvedSymbolException | UnsupportedOperationException ex) { - // UnsupportedOperationException: type is a type variable - // See TargetMethodFinderVisitor.visit(MethodDeclaration, Void) for more details - return false; - } catch (RuntimeException e) { - // The current class is employed by the target methods, although not all of its members are - // utilized. It's not surprising for unused members to remain unresolved. - // If this constructor is from the parent of the current class, and it is not resolved, we - // will get a RuntimeException, otherwise just a UnsolvedSymbolException. - // Copied from PrunerVisitor.visit(ConstructorDeclaration, Void) - return false; - } - } else if (node instanceof MethodDeclaration) { - try { - qualifiedName = ((MethodDeclaration) node).resolve().getQualifiedSignature(); - } catch (UnsolvedSymbolException | UnsupportedOperationException ex) { - // UnsupportedOperationException: type is a type variable - // See TargetMethodFinderVisitor.visit(MethodDeclaration, Void) for more details - return false; - } - } else if (node instanceof FieldDeclaration) { - try { - FieldDeclaration decl = (FieldDeclaration) node; - for (VariableDeclarator var : decl.getVariables()) { - qualifiedName = JavaParserUtil.getEnclosingClassName(decl) + "#" + var.getNameAsString(); - if (usedMembers.contains(qualifiedName) || targetFields.contains(qualifiedName)) { - return true; - } - } - } catch (UnsolvedSymbolException ex) { - return false; - } - return false; - } else { - return false; - } - - if (qualifiedName.contains(" ")) { - qualifiedName = qualifiedName.replaceAll("//s", ""); - } - - if (isClass) { - return usedTypeElements.contains(qualifiedName); - } else { - // fields should already be handled at this point - return usedMembers.contains(qualifiedName) || targetMethods.contains(qualifiedName); - } - } -} diff --git a/src/main/java/org/checkerframework/specimin/StandardTypeRuleDependencyMap.java b/src/main/java/org/checkerframework/specimin/StandardTypeRuleDependencyMap.java new file mode 100644 index 000000000..3230f4ef8 --- /dev/null +++ b/src/main/java/org/checkerframework/specimin/StandardTypeRuleDependencyMap.java @@ -0,0 +1,644 @@ +package org.checkerframework.specimin; + +import com.github.javaparser.ast.CompilationUnit; +import com.github.javaparser.ast.Node; +import com.github.javaparser.ast.body.AnnotationMemberDeclaration; +import com.github.javaparser.ast.body.BodyDeclaration; +import com.github.javaparser.ast.body.CallableDeclaration; +import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration; +import com.github.javaparser.ast.body.ConstructorDeclaration; +import com.github.javaparser.ast.body.EnumConstantDeclaration; +import com.github.javaparser.ast.body.FieldDeclaration; +import com.github.javaparser.ast.body.MethodDeclaration; +import com.github.javaparser.ast.body.TypeDeclaration; +import com.github.javaparser.ast.body.VariableDeclarator; +import com.github.javaparser.ast.expr.AnnotationExpr; +import com.github.javaparser.ast.expr.Expression; +import com.github.javaparser.ast.expr.ObjectCreationExpr; +import com.github.javaparser.ast.nodeTypes.NodeWithAnnotations; +import com.github.javaparser.ast.nodeTypes.NodeWithExtends; +import com.github.javaparser.ast.nodeTypes.NodeWithImplements; +import com.github.javaparser.ast.nodeTypes.NodeWithModifiers; +import com.github.javaparser.ast.nodeTypes.NodeWithParameters; +import com.github.javaparser.ast.nodeTypes.NodeWithSimpleName; +import com.github.javaparser.ast.nodeTypes.NodeWithThrownExceptions; +import com.github.javaparser.ast.nodeTypes.NodeWithType; +import com.github.javaparser.ast.nodeTypes.NodeWithTypeArguments; +import com.github.javaparser.ast.nodeTypes.NodeWithTypeParameters; +import com.github.javaparser.ast.stmt.BlockStmt; +import com.github.javaparser.ast.stmt.Statement; +import com.github.javaparser.ast.type.ClassOrInterfaceType; +import com.github.javaparser.resolution.UnsolvedSymbolException; +import com.github.javaparser.resolution.declarations.ResolvedConstructorDeclaration; +import com.github.javaparser.resolution.declarations.ResolvedEnumConstantDeclaration; +import com.github.javaparser.resolution.declarations.ResolvedFieldDeclaration; +import com.github.javaparser.resolution.declarations.ResolvedMethodDeclaration; +import com.github.javaparser.resolution.declarations.ResolvedMethodLikeDeclaration; +import com.github.javaparser.resolution.declarations.ResolvedReferenceTypeDeclaration; +import com.github.javaparser.resolution.declarations.ResolvedTypeParameterDeclaration; +import com.github.javaparser.resolution.types.ResolvedReferenceType; +import com.github.javaparser.resolution.types.ResolvedType; +import com.github.javaparser.symbolsolver.javaparsermodel.declarations.DefaultConstructorDeclaration; +import com.github.javaparser.utils.Pair; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** The standard type rule dependency map */ +public class StandardTypeRuleDependencyMap implements TypeRuleDependencyMap { + + /** + * A map of fully-qualified names to compilation units, used to find declarations that are + * properly attached to a compilation unit. + */ + private final Map fqnToCompilationUnits; + + /** + * A map of abstract super method qualified signatures to their concrete implementations. This + * addresses cases where an abstract method is preserved because it is directly called but its + * concrete implementations are not, but the types containing those concrete implementations are + * also included in the slice. If we encounter the abstract super method after encountering the + * concrete implementation, then we use this map. + */ + private final Map> methodsWithAbstractSuperDefinitions = + new HashMap<>(); + + /** + * Similar to {@link #methodsWithAbstractSuperDefinitions}, but for cases where the type of the + * concrete implementation is seen after the abstract super method. + */ + private final Set nonJDKMustImplementMethods = new HashSet<>(); + + /** + * Creates a new StandardTypeRuleDependencyMap to be passed into Slicer. + * + * @param fqnToCompilationUnits The map of type FQNs to their compilation units. + */ + public StandardTypeRuleDependencyMap(Map fqnToCompilationUnits) { + this.fqnToCompilationUnits = fqnToCompilationUnits; + } + + /** + * Given a node, return all relevant nodes based on its type. + * + * @param node The node + * @return All relevant nodes to the input node. For example, this could be annotations, type + * parameters, parameters, return type, etc. for methods. + */ + @Override + public List getRelevantElements(Node node) { + List elements = new ArrayList<>(); + + if (node instanceof NodeWithAnnotations withAnnotations) { + for (AnnotationExpr annotation : withAnnotations.getAnnotations()) { + if (annotation.toString().equals("@Override")) { + // Never preserve @Override, since it causes compile errors but does not fix them. + continue; + } else if (annotation.toString().equals("@FunctionalInterface")) { + // Don't preserve @FunctionalInterface until we know the method is also preserved. + continue; + } + elements.add(annotation); + } + } + if (node instanceof NodeWithModifiers withModifiers) { + elements.addAll(withModifiers.getModifiers()); + } + if (node instanceof NodeWithTypeArguments withTypeArguments + && withTypeArguments.getTypeArguments().isPresent()) { + elements.addAll(withTypeArguments.getTypeArguments().get()); + } + if (node instanceof NodeWithTypeParameters withTypeParameters) { + elements.addAll(withTypeParameters.getTypeParameters()); + } + // i.e., method declarations, parameters, annotation type declarations, instanceof, etc. + if (node instanceof NodeWithType withType) { + elements.add(withType.getType()); + + if (withType instanceof MethodDeclaration methodDecl && methodDecl.isAbstract()) { + ResolvedMethodDeclaration resolvedMethod = methodDecl.resolve(); + nonJDKMustImplementMethods.add(resolvedMethod); + if (methodsWithAbstractSuperDefinitions.containsKey( + resolvedMethod.getQualifiedSignature())) { + elements.addAll( + methodsWithAbstractSuperDefinitions.get(resolvedMethod.getQualifiedSignature())); + } + } + } + if (node instanceof NodeWithSimpleName nodeWithSimpleName) { + elements.add(nodeWithSimpleName.getName()); + } + + // Type declarations + if (node instanceof NodeWithImplements withImplements) { + elements.addAll(withImplements.getImplementedTypes()); + } + if (node instanceof NodeWithExtends withExtends) { + elements.addAll(withExtends.getExtendedTypes()); + } + if (node instanceof TypeDeclaration typeDeclaration) { + List mustImplement = + JavaParserUtil.getDeclarationsForAllMustImplementMethods( + typeDeclaration, nonJDKMustImplementMethods, fqnToCompilationUnits); + elements.addAll(mustImplement); + + for (MethodDeclaration method : typeDeclaration.getMethods()) { + if (mustImplement.contains(method)) { + continue; + } + try { + ResolvedMethodDeclaration resolvedMethod = method.resolve(); + if (resolvedMethod.isAbstract()) { + continue; + } + + for (ResolvedReferenceType ancestor : resolvedMethod.declaringType().getAllAncestors()) { + // Approximate: check to see if name and arity matches. + for (ResolvedMethodDeclaration ancestorMethod : + ancestor.getTypeDeclaration().get().getDeclaredMethods()) { + if (ancestorMethod.getName().equals(resolvedMethod.getName()) + && ancestorMethod.getNumberOfParams() == resolvedMethod.getNumberOfParams() + && ancestorMethod.isAbstract()) { + methodsWithAbstractSuperDefinitions + .computeIfAbsent(ancestorMethod.getQualifiedSignature(), k -> new HashSet<>()) + .add(method); + } + } + } + } catch (UnsolvedSymbolException ex) { + // Ignore + } + } + } + + // If the node is a type declaration, exit now, so we don't unintentionally + // add extra nodes to our worklist. + if (node instanceof TypeDeclaration) { + return elements; + } + + // ========================================================= + + // Method declarations + + // i.e., constructor/method declarations, lambdas + if (node instanceof NodeWithParameters withParameters) { + elements.addAll(withParameters.getParameters()); + } + + // i.e., constructor/method declarations + if (node instanceof NodeWithThrownExceptions withThrownExceptions) { + elements.addAll(withThrownExceptions.getThrownExceptions()); + } + + // If this is a method declaration in a functional interface, preserve the + // "@FunctionalInterface" annotation. + if (node instanceof MethodDeclaration methodDeclaration + && JavaParserUtil.getEnclosingClassLikeOptional(methodDeclaration) + instanceof ClassOrInterfaceDeclaration typeDecl + && typeDecl.isInterface() + && typeDecl.getAnnotationByName("FunctionalInterface").isPresent()) { + elements.add(typeDecl.getAnnotationByName("FunctionalInterface").get()); + } + + if (node instanceof ConstructorDeclaration constructor) { + TypeDeclaration type = JavaParserUtil.getEnclosingClassLike(node); + + if (type instanceof NodeWithExtends withExtends + && withExtends.getExtendedTypes().size() == 1) { + // First, check if the superclass is resolvable. If it is, then check to see if there is a + // default constructor. If not, then we must preserve the explicit constructor invocation + // statement in all constructors that are being preserved. + + ClassOrInterfaceType parentType = withExtends.getExtendedTypes().get(0); + try { + ResolvedType parentResolvedType = parentType.resolve(); + + if (parentResolvedType.isReferenceType() + && parentResolvedType.asReferenceType().getTypeDeclaration().isPresent()) { + ResolvedReferenceTypeDeclaration parentDecl = + parentResolvedType.asReferenceType().getTypeDeclaration().get(); + + boolean hasDefaultConstructor = parentDecl.getConstructors().size() == 0; + + for (ResolvedConstructorDeclaration resolvedConstructor : + parentDecl.getConstructors()) { + if (resolvedConstructor.getNumberOfParams() == 0) { + hasDefaultConstructor = true; + break; + } + } + + if (!hasDefaultConstructor) { + elements.add(constructor.getBody()); + + // No default constructor = first statement must be super()/this() + Statement firstStatement = + constructor.getBody().getStatements().stream() + .filter(s -> s.isExplicitConstructorInvocationStmt()) + .findFirst() + .get(); + + elements.add(firstStatement); + } + } + } catch (UnsolvedSymbolException ex) { + // Always preserve super/this if the parent type is not resolvable, since we don't + // know if there is a default constructor. See UnsolvedSuperConstructor2Test for an + // example of why this is necessary. + + Statement firstStatement = + constructor.getBody().getStatements().stream() + .filter(s -> s.isExplicitConstructorInvocationStmt()) + .findFirst() + .orElse(null); + + if (firstStatement != null) { + elements.add(constructor.getBody()); + elements.add(firstStatement); + } + } + } + } + + // If the node is a member declaration, exit now, so we don't unintentionally + // add extra nodes to our worklist. + if (node instanceof CallableDeclaration) { + return elements; + } + + // ========================================================= + + // Statements + // ** If a statement is included in the slice, then that means it is in one + // of the target members. Therefore, its children are always relevant. + + // Never add variable declarators here: this prevents extra variables + // from being included when a single field declaration has multiple variable + // declarators. + if (node instanceof FieldDeclaration fieldDecl) { + for (Node child : fieldDecl.getChildNodes()) { + if (!(child instanceof VariableDeclarator)) { + elements.add(child); + } + } + return elements; + } + + // If a constructor's block statement is included, we shouldn't add all its child statements + // because we only want + // to preserve super(). If this is the target, TargetMemberFinderVisitor will have added the + // statements already. + if (node instanceof BlockStmt + && node.getParentNode().orElse(null) instanceof ConstructorDeclaration) { + return elements; + } + + if (node instanceof VariableDeclarator varDecl + && varDecl.getInitializer().isPresent() + && node.getParentNode().get() instanceof FieldDeclaration) { + // For field declarations, don't add the initializer + Expression initializer = varDecl.getInitializer().get(); + + for (Node child : varDecl.getChildNodes()) { + if (child.equals(initializer)) { + continue; + } + + elements.add(child); + } + + return elements; + } + + elements.addAll(node.getChildNodes()); + + if (node instanceof ObjectCreationExpr objectCreationExpr + && objectCreationExpr.getAnonymousClassBody().isPresent()) { + List> anonymousClassBody = + objectCreationExpr.getAnonymousClassBody().get(); + // Must preserve everything in the anonymous class body + for (BodyDeclaration bodyDeclaration : anonymousClassBody) { + elements.add(bodyDeclaration); + + // Need to call getChildNodes() since adding a method/field/constructor declaration + // will not add its content. Still, we must be careful about adding @Override. + for (Node child : bodyDeclaration.getChildNodes()) { + if (child instanceof AnnotationExpr annotation + && annotation.toString().equals("@Override")) { + continue; + } + + elements.add(child); + + // By default, variable declarator initializers are not preserved + if (child instanceof VariableDeclarator varDecl && varDecl.getInitializer().isPresent()) { + elements.add(varDecl.getInitializer().get()); + } + } + } + } + + return elements; + } + + @Override + public List getRelevantElements(Object resolved) { + List elements = new ArrayList<>(); + + if (resolved instanceof ResolvedType resolvedType) { + if (resolvedType.isArray()) { + resolvedType = resolvedType.asArrayType().getComponentType(); + } + if (resolvedType.isReferenceType() + && resolvedType.asReferenceType().getTypeDeclaration().isPresent()) { + return getRelevantElements(resolvedType.asReferenceType().getTypeDeclaration().get()); + } + } + + if (resolved instanceof ResolvedReferenceTypeDeclaration resolvedTypeDeclaration) { + TypeDeclaration type = + JavaParserUtil.getTypeFromQualifiedName( + resolvedTypeDeclaration.getQualifiedName(), fqnToCompilationUnits); + + if (type == null) { + return elements; + } + + // Ensure outer classes are included in the slice + TypeDeclaration outerType = JavaParserUtil.getEnclosingClassLikeOptional(type); + + // Don't get all the outer classes, since it's redundant. Once this added outerType + // is handled in the worklist, it will add the next outer class, and so on. + if (outerType != null) { + elements.add(outerType); + } + + // Unfortunately, JavaParser doesn't allow us to solve annotation member value pairs, + // so we can't tell what is used and what isn't. Preserve all annotation members for + // now until we figure out a better solution/JavaParser adds support. + if (resolvedTypeDeclaration.isAnnotation()) { + elements.addAll( + resolvedTypeDeclaration.asAnnotation().getAnnotationMembers().stream() + .map( + member -> + type.findFirst( + AnnotationMemberDeclaration.class, + n -> n.getNameAsString().equals(member.getName())) + .get()) + .toList()); + } + + elements.add(type); + } + + if (resolved instanceof ResolvedMethodLikeDeclaration resolvedMethodLikeDeclaration) { + TypeDeclaration type; + boolean isAnonymousClass = false; + List> typeParametersMapForAnonClass = + null; + // Check to see if this method is in an anonymous class + if (resolvedMethodLikeDeclaration.toAst().isPresent() + && resolvedMethodLikeDeclaration.toAst().get().getParentNode().get() + instanceof ObjectCreationExpr objCreationExpr) { + try { + // Try to get the parent class of the anonymous class + ResolvedType resolvedAnonParent = objCreationExpr.getType().resolve(); + type = + JavaParserUtil.getTypeFromQualifiedName( + resolvedAnonParent.describe(), fqnToCompilationUnits); + typeParametersMapForAnonClass = + resolvedAnonParent.asReferenceType().getTypeParametersMap(); + isAnonymousClass = true; + } catch (UnsolvedSymbolException ex) { + // Handle in UnsolvedSymbolGenerator + return elements; + } + } else { + type = + JavaParserUtil.getTypeFromQualifiedName( + resolvedMethodLikeDeclaration.declaringType().getQualifiedName(), + fqnToCompilationUnits); + } + + if (type == null) { + return elements; + } + + if (resolved instanceof ResolvedMethodDeclaration resolvedMethodDeclaration) { + if (isAnonymousClass && typeParametersMapForAnonClass != null) { + // The current type is already a parent class, so we need to add those too + List methods = new ArrayList<>(); + addOverriddenMethodsToList( + type, resolvedMethodDeclaration, typeParametersMapForAnonClass, methods); + elements.addAll(methods); + } + + elements.addAll(getAllOverriddenMethods(resolvedMethodDeclaration, type)); + } + + // Case: new Foo() but Foo does not contain a constructor + // Anonymous class methods do not need to be re-added + if (!(resolved instanceof DefaultConstructorDeclaration) + && !isAnonymousClass + && resolvedMethodLikeDeclaration.toAst().isPresent()) { + Node unattached = resolvedMethodLikeDeclaration.toAst().get(); + CallableDeclaration methodLike = + type.findFirst(CallableDeclaration.class, n -> n.equals(unattached)).get(); + + elements.add(methodLike); + } + + elements.add(type); + } + + if (resolved instanceof ResolvedFieldDeclaration resolvedFieldDeclaration) { + TypeDeclaration type = + JavaParserUtil.getTypeFromQualifiedName( + resolvedFieldDeclaration.declaringType().getQualifiedName(), fqnToCompilationUnits); + + if (type == null) { + return elements; + } + + Node unattached = resolvedFieldDeclaration.toAst().get(); + FieldDeclaration field = + type.findFirst(FieldDeclaration.class, n -> n.equals(unattached)).get(); + + VariableDeclarator variableDeclarator = + field.getVariables().stream() + .filter(var -> var.getNameAsString().equals(resolvedFieldDeclaration.getName())) + .findFirst() + .get(); + + elements.add(type); + elements.add(field); + elements.add(variableDeclarator); + } + + if (resolved instanceof ResolvedEnumConstantDeclaration resolvedEnumConstantDeclaration + && resolvedEnumConstantDeclaration.toAst().isPresent()) { + TypeDeclaration type = + JavaParserUtil.getTypeFromQualifiedName( + resolvedEnumConstantDeclaration.getType().describe(), fqnToCompilationUnits); + + if (type == null) { + return elements; + } + + Node unattached = resolvedEnumConstantDeclaration.toAst().get(); + EnumConstantDeclaration enumConstant = + type.findFirst(EnumConstantDeclaration.class, n -> n.equals(unattached)).get(); + + elements.add(type); + elements.add(enumConstant); + + // Most of the time, this method will return one constructor. However, there may be cases + // where we include multiple constructors, but this is because we simply don't know which + // one to use. + List> constructors = + JavaParserUtil.tryResolveNodeWithUnresolvableArguments( + enumConstant, fqnToCompilationUnits); + elements.addAll(constructors); + } + + return elements; + } + + /** + * Gets all overridden methods of the given method declaration, including those in ancestors. + * + * @param original The original method declaration to find overridden methods for + * @param type The type declaration to search for overridden methods in + * @return A list of all overridden methods + */ + private List getAllOverriddenMethods( + ResolvedMethodDeclaration original, TypeDeclaration type) { + List result = new ArrayList<>(); + + getAllOverriddenMethodsImpl(original, type, result); + return result; + } + + /** + * Helper method for {@link #getAllOverriddenMethods(ResolvedMethodDeclaration, TypeDeclaration)}. + * This method recursively finds all overridden methods in the type declaration's ancestors. + * + * @param original The original method declaration to find overridden methods for + * @param type The type declaration to search for overridden methods in + * @param result A list to collect all overridden methods found + */ + private void getAllOverriddenMethodsImpl( + ResolvedMethodDeclaration original, TypeDeclaration type, List result) { + List parents = JavaParserUtil.getDirectSuperTypes(type); + + for (ClassOrInterfaceType parent : parents) { + ResolvedType parentType; + try { + parentType = parent.resolve(); + } catch (UnsolvedSymbolException ex) { + continue; + } + + if (!parentType.isReferenceType() + || parentType.asReferenceType().getTypeDeclaration().isEmpty()) { + continue; + } + + ResolvedReferenceTypeDeclaration decl = + parentType.asReferenceType().getTypeDeclaration().get(); + + TypeDeclaration typeDecl = + JavaParserUtil.getTypeFromQualifiedName(decl.getQualifiedName(), fqnToCompilationUnits); + if (typeDecl == null) { + continue; + } + + addOverriddenMethodsToList( + typeDecl, original, parentType.asReferenceType().getTypeParametersMap(), result); + + getAllOverriddenMethodsImpl(original, typeDecl, result); + } + } + + /** + * Helper method to add methods of matching signature to {@code original} from {@code typeDecl} to + * {@code result}. + * + * @param typeDecl The type declaration to search for overridden methods in + * @param original The original method declaration to find overridden methods for + * @param typeParametersMap The type parameters map + * @param result A list to collect all overridden methods found + */ + private void addOverriddenMethodsToList( + TypeDeclaration typeDecl, + ResolvedMethodDeclaration original, + List> typeParametersMap, + List result) { + for (MethodDeclaration method : typeDecl.getMethods()) { + try { + if (original + .getSignature() + .equals( + JavaParserUtil.getSignatureFromResolvedMethodWithTypeVariablesMap( + method.resolve(), typeParametersMap))) { + result.add(method); + } + } catch (UnsolvedSymbolException ex) { + // At least one parameter type may not be solvable. In this case, try comparing + // simple names. + if (areAstAndResolvedMethodLikelyEqual(original, method)) { + result.add(method); + } + } + } + } + + /** + * Checks to see if a resolved method declaration and a method declaration AST node are likely to + * be the same method, based on their names and the simple names of their parameters. Use this + * method only when {@code ast} is not resolvable and you can't compare with qualified parameter + * types. + * + * @param resolved The resolved method declaration + * @param ast The method declaration AST node + * @return true if the method and AST node are likely to be the same method, false otherwise + */ + private boolean areAstAndResolvedMethodLikelyEqual( + ResolvedMethodDeclaration resolved, MethodDeclaration ast) { + if (!ast.getNameAsString().equals(resolved.getName())) { + return false; + } + + if (ast.getParameters().size() != resolved.getNumberOfParams()) { + return false; + } + + for (int i = 0; i < ast.getParameters().size(); i++) { + String resolvedParamType; + try { + resolvedParamType = resolved.getParam(i).getType().describe(); + } catch (UnsolvedSymbolException ex) { + // See if the AST version exists, and use that simple name + if (resolved.toAst().orElse(null) instanceof MethodDeclaration methodDecl) { + resolvedParamType = methodDecl.getParameter(i).getType().toString(); + } else { + // If we cannot compare, we'll return false + return false; + } + } + + if (!JavaParserUtil.getSimpleNameFromQualifiedName(resolvedParamType) + .equals( + JavaParserUtil.getSimpleNameFromQualifiedName( + ast.getParameter(i).getType().toString()))) { + return false; + } + } + + return true; + } +} diff --git a/src/main/java/org/checkerframework/specimin/TargetMemberFinderVisitor.java b/src/main/java/org/checkerframework/specimin/TargetMemberFinderVisitor.java index c14b8b13d..7b093697d 100644 --- a/src/main/java/org/checkerframework/specimin/TargetMemberFinderVisitor.java +++ b/src/main/java/org/checkerframework/specimin/TargetMemberFinderVisitor.java @@ -1,43 +1,20 @@ package org.checkerframework.specimin; import com.github.javaparser.ast.Node; -import com.github.javaparser.ast.PackageDeclaration; +import com.github.javaparser.ast.body.AnnotationDeclaration; +import com.github.javaparser.ast.body.CallableDeclaration; import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration; import com.github.javaparser.ast.body.ConstructorDeclaration; -import com.github.javaparser.ast.body.EnumConstantDeclaration; import com.github.javaparser.ast.body.EnumDeclaration; import com.github.javaparser.ast.body.FieldDeclaration; import com.github.javaparser.ast.body.MethodDeclaration; -import com.github.javaparser.ast.body.Parameter; +import com.github.javaparser.ast.body.RecordDeclaration; +import com.github.javaparser.ast.body.TypeDeclaration; import com.github.javaparser.ast.body.VariableDeclarator; -import com.github.javaparser.ast.expr.AssignExpr; -import com.github.javaparser.ast.expr.Expression; -import com.github.javaparser.ast.expr.FieldAccessExpr; -import com.github.javaparser.ast.expr.LambdaExpr; -import com.github.javaparser.ast.expr.MethodCallExpr; -import com.github.javaparser.ast.expr.MethodReferenceExpr; -import com.github.javaparser.ast.expr.NameExpr; -import com.github.javaparser.ast.expr.ObjectCreationExpr; -import com.github.javaparser.ast.expr.SuperExpr; -import com.github.javaparser.ast.stmt.CatchClause; -import com.github.javaparser.ast.stmt.ExplicitConstructorInvocationStmt; -import com.github.javaparser.ast.type.ClassOrInterfaceType; -import com.github.javaparser.ast.type.ReferenceType; -import com.github.javaparser.ast.type.Type; -import com.github.javaparser.ast.type.UnionType; +import com.github.javaparser.ast.stmt.BlockStmt; +import com.github.javaparser.ast.visitor.ModifierVisitor; import com.github.javaparser.ast.visitor.Visitable; -import com.github.javaparser.resolution.MethodUsage; -import com.github.javaparser.resolution.UnsolvedSymbolException; -import com.github.javaparser.resolution.declarations.ResolvedConstructorDeclaration; -import com.github.javaparser.resolution.declarations.ResolvedEnumConstantDeclaration; -import com.github.javaparser.resolution.declarations.ResolvedFieldDeclaration; -import com.github.javaparser.resolution.declarations.ResolvedMethodDeclaration; -import com.github.javaparser.resolution.declarations.ResolvedParameterDeclaration; -import com.github.javaparser.resolution.declarations.ResolvedTypeParameterDeclaration; -import com.github.javaparser.resolution.declarations.ResolvedValueDeclaration; -import com.github.javaparser.resolution.types.ResolvedReferenceType; -import com.github.javaparser.resolution.types.ResolvedType; -import com.github.javaparser.resolution.types.ResolvedWildcard; +import java.util.Deque; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -45,12 +22,15 @@ import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; +import org.checkerframework.checker.signature.qual.ClassGetSimpleName; +import org.checkerframework.specimin.modularity.ModularityModel; /** * The main visitor for Specimin's first phase, which locates the target member(s) and compiles - * information on what specifications they use. + * information on what specifications they use and adds them to the worklist for later use in the + * slice. */ -public class TargetMemberFinderVisitor extends SpeciminStateVisitor { +public class TargetMemberFinderVisitor extends ModifierVisitor { /** * The names of the target methods. The format is * class.fully.qualified.Name#methodName(Param1Type, Param2Type, ...). All the names will have @@ -58,8 +38,14 @@ public class TargetMemberFinderVisitor extends SpeciminStateVisitor { */ private final Set targetMethodNames; - /** The name of the package currently being visited. */ - private String currentPackage = ""; + /** The names of the target fields. The format is class.fully.qualified.Name#fieldName. */ + private final Set targetFields; + + /** The simple name of the class currently visited */ + protected @ClassGetSimpleName String className = ""; + + /** The qualified name of the class currently being visited. */ + protected String currentClassQualifiedName = ""; /** * The keys of this map are a local copy of the input list of methods. A method is removed from @@ -74,48 +60,39 @@ public class TargetMemberFinderVisitor extends SpeciminStateVisitor { /** Same as the unfoundMethods set, but for fields */ private final Map> unfoundFields = new HashMap<>(); - /** - * This map connects the resolved declaration of a method to the interface that contains it, if - * any. - */ - private final Map - methodDeclarationToInterfaceType = new HashMap<>(); + /** The worklist to later be passed into the slicer. */ + private final Deque worklist; - /** - * This map connects the fully-qualified names of non-primary classes with the fully-qualified - * names of their corresponding primary classes. A primary class is a class that has the same name - * as the Java file where the class is declared. - */ - Map nonPrimaryClassesToPrimaryClass; - - /** - * JavaParser is not perfect. Sometimes it can't solve resolved method calls if they have - * complicated type variables or if the receiver is the parameter of a lambda expression. We keep - * track of these stuck method calls and preserve them anyway. There are two possible formats for - * the strings in this set: fully-qualified method names (which will be directly preserved) and - * unqualified method names with a {@literal @} symbol and the number of parameters that they take - * appended. Anything that matches the latter will later be preserved. - */ - private final Set resolvedYetStuckMethodCall = new HashSet<>(); + /** The modularity model to use. */ + private final ModularityModel modularityModel; /** * Create a new target method finding visitor. * - * @param previous the previous Specimin visitor - * @param nonPrimaryClassesToPrimaryClass map connecting non-primary classes with their - * corresponding primary classes + * @param targetMethods The set of methods to preserve + * @param targetFields The set of fields to preserve + * @param worklist The worklist + * @param modularityModel The modularity model to use */ public TargetMemberFinderVisitor( - SpeciminStateVisitor previous, Map nonPrimaryClassesToPrimaryClass) { - super(previous); - targetMethodNames = new HashSet<>(); + List targetMethods, + List targetFields, + Deque worklist, + ModularityModel modularityModel) { + this.modularityModel = modularityModel; + this.targetMethodNames = new HashSet(); + for (String methodSignature : targetMethods) { this.targetMethodNames.add(methodSignature.replaceAll("\\s", "")); } + + this.targetFields = new HashSet<>(targetFields); + unfoundMethods = new HashMap<>(targetMethods.size()); - targetMethodNames.forEach(m -> unfoundMethods.put(m, new HashSet<>())); - targetFields.forEach(f -> unfoundFields.put(f, new HashSet<>())); - this.nonPrimaryClassesToPrimaryClass = nonPrimaryClassesToPrimaryClass; + this.targetMethodNames.forEach(m -> unfoundMethods.put(m, new HashSet<>())); + this.targetFields.forEach(f -> unfoundFields.put(f, new HashSet<>())); + + this.worklist = worklist; } /** @@ -144,29 +121,6 @@ public Map> getUnfoundFields() { return unfoundFields; } - /** - * Get the set of resolved yet stuck method calls. - * - * @return the value of stuck methods. - */ - public Set getResolvedYetStuckMethodCall() { - return resolvedYetStuckMethodCall; - } - - /** - * Updates the mapping of method declarations to their corresponding interface type based on a - * list of methods and the interface type that contains those methods. - * - * @param methodList the list of resolved method declarations - * @param interfaceType the interface containing the specified methods. - */ - private void updateMethodDeclarationToInterfaceType( - List methodList, ClassOrInterfaceType interfaceType) { - for (ResolvedMethodDeclaration method : methodList) { - this.methodDeclarationToInterfaceType.put(method, interfaceType); - } - } - /** * Updates unfoundMethods so that the appropriate elements have their set of considered methods * updated to match a method that was not a target method. @@ -210,25 +164,65 @@ private void updateUnfoundFields(String fieldAsString) { } @Override - public Visitable visit(PackageDeclaration decl, Void p) { - this.currentPackage = decl.getNameAsString(); - return super.visit(decl, p); + public Visitable visit(ClassOrInterfaceDeclaration decl, Void p) { + return handleClassLikeDeclaration(decl, p); } @Override - public Visitable visit(ClassOrInterfaceDeclaration decl, Void p) { - for (ClassOrInterfaceType interfaceType : decl.getImplementedTypes()) { - try { - updateMethodDeclarationToInterfaceType( - JavaParserUtil.classOrInterfaceTypeToResolvedReferenceType(interfaceType) - .getAllMethods(), - interfaceType); - } catch (UnsolvedSymbolException e) { - continue; - } + public Visitable visit(EnumDeclaration decl, Void p) { + return handleClassLikeDeclaration(decl, p); + } + + @Override + public Visitable visit(AnnotationDeclaration decl, Void p) { + return handleClassLikeDeclaration(decl, p); + } + + @Override + public Visitable visit(RecordDeclaration decl, Void p) { + return handleClassLikeDeclaration(decl, p); + } + + /** + * The purpose of this method is to help find the fully qualified name, not to do any modification + * to the worklist/slice. + * + * @param decl The type declaration being visited + * @param p void param + * @return super.visit() return value + */ + private Visitable handleClassLikeDeclaration(TypeDeclaration decl, Void p) { + if (decl.isNestedType()) { + this.currentClassQualifiedName += "." + decl.getName().asString(); + } else if (!JavaParserUtil.isLocalClassDecl(decl)) { + // the purpose of keeping track of class name is to recognize the signatures of target + // methods. Since we don't support methods inside local classes as target methods, we don't + // need to keep track of class name in this case. + this.currentClassQualifiedName = decl.getFullyQualifiedName().orElseThrow(); + } + + Visitable result; + if (decl instanceof ClassOrInterfaceDeclaration cid) { + result = super.visit(cid, p); + } else if (decl instanceof EnumDeclaration ed) { + result = super.visit(ed, p); + } else if (decl instanceof AnnotationDeclaration ad) { + result = super.visit(ad, p); + } else if (decl instanceof RecordDeclaration rd) { + result = super.visit(rd, p); + } else { + throw new IllegalArgumentException("Unsupported TypeDeclaration: " + decl.getClass()); } - return super.visit(decl, p); + if (decl.isNestedType()) { + this.currentClassQualifiedName = + this.currentClassQualifiedName.substring( + 0, this.currentClassQualifiedName.lastIndexOf('.')); + } else if (!JavaParserUtil.isLocalClassDecl(decl)) { + this.currentClassQualifiedName = ""; + } + + return result; } @Override @@ -239,22 +233,17 @@ public Visitable visit(ConstructorDeclaration method, Void p) { // remove spaces methodName = methodName.replaceAll("\\s", ""); if (this.targetMethodNames.contains(methodName)) { - ResolvedConstructorDeclaration resolvedMethod = method.resolve(); - targetMethods.add(resolvedMethod.getQualifiedSignature()); unfoundMethods.remove(methodName); - updateUsedClassWithQualifiedClassName( - JavaParserUtil.packagePrefix(resolvedMethod) + resolvedMethod.getClassName(), - usedTypeElements, - nonPrimaryClassesToPrimaryClass); + addMethodAndChildrenToWorklist(method); + if (modularityModel.preserveAllFieldsIfTargetIsConstructor()) { // This cast is safe, because a constructor must be contained in a class declaration. ClassOrInterfaceDeclaration thisClass = (ClassOrInterfaceDeclaration) JavaParserUtil.getEnclosingClassLike(method); for (FieldDeclaration field : thisClass.getFields()) { + worklist.add(field); for (VariableDeclarator variable : field.getVariables()) { - usedMembers.add(currentClassQualifiedName + "#" + variable.getNameAsString()); - ResolvedType fieldType = variable.resolve().getType(); - updateUsedClassBasedOnType(fieldType); + worklist.add(variable); } } } @@ -262,673 +251,66 @@ public Visitable visit(ConstructorDeclaration method, Void p) { updateUnfoundMethods(methodName); } - Visitable result = super.visit(method, p); - - if (method.getParentNode().isEmpty()) { - return result; - } - if (method.getParentNode().get() instanceof EnumDeclaration) { - EnumDeclaration parentNode = (EnumDeclaration) method.getParentNode().get(); - if (parentNode.getFullyQualifiedName().isEmpty()) { - return result; - } - // used enums needs to have compilable constructors. - if (usedTypeElements.contains(parentNode.getFullyQualifiedName().orElseThrow())) { - for (Parameter parameter : method.getParameters()) { - updateUsedClassBasedOnType(parameter.getType().resolve()); - } - } - } - return result; - } - - @Override - public Visitable visit(VariableDeclarator node, Void arg) { - if (node.getParentNode().isPresent() - && node.getParentNode().get() instanceof FieldDeclaration) { - String fieldName = this.currentClassQualifiedName + "#" + node.getNameAsString(); - if (targetFields.contains(fieldName)) { - ResolvedFieldDeclaration resolvedField = - ((FieldDeclaration) node.getParentNode().get()).resolve(); - unfoundFields.remove(fieldName); - updateUsedClassWithQualifiedClassName( - resolvedField.declaringType().getQualifiedName(), - usedTypeElements, - nonPrimaryClassesToPrimaryClass); - } else { - updateUnfoundFields(fieldName); - } - } - return super.visit(node, arg); - } - - @Override - public Visitable visit(AssignExpr node, Void p) { - if (insideTargetCtor) { - // check if the LHS is a field - Expression lhs = node.getTarget(); - if (lhs.isFieldAccessExpr()) { - FieldAccessExpr asFieldAccess = lhs.asFieldAccessExpr(); - Expression scope = asFieldAccess.getScope(); - if (scope.toString().equals("this")) { - fieldsAssignedByTargetCtors.add( - currentClassQualifiedName + "#" + asFieldAccess.getNameAsString()); - } - } else if (lhs.isNameExpr()) { - // could be a field of "this" - NameExpr asName = lhs.asNameExpr(); - ResolvedValueDeclaration resolved = asName.resolve(); - if (resolved.isField()) { - fieldsAssignedByTargetCtors.add( - currentClassQualifiedName + "#" + asName.getNameAsString()); - } - } - } - return super.visit(node, p); + return super.visit(method, p); } @Override public Visitable visit(MethodDeclaration method, Void p) { - boolean oldInsideTargetMember = insideTargetMember; - // TODO: test this with annotations String methodWithoutReturnAndAnnos = JavaParserUtil.removeMethodReturnTypeAndAnnotations(method); String methodName = this.currentClassQualifiedName + "#" + methodWithoutReturnAndAnnos; - // this method belongs to an anonymous class inside the target method - if (insideTargetMember) { - Node parentNode = method.getParentNode().get(); - // it could also be an enum declaration, but those are handled separately - if (parentNode instanceof ObjectCreationExpr) { - ObjectCreationExpr parentExpression = (ObjectCreationExpr) parentNode; - ResolvedConstructorDeclaration resolved = parentExpression.resolve(); - String methodPackagePrefix = JavaParserUtil.packagePrefix(resolved); - String methodClass = resolved.getClassName(); - usedMembers.add(methodPackagePrefix + methodClass + "." + method.getNameAsString() + "()"); - updateUsedClassWithQualifiedClassName( - methodPackagePrefix + methodClass, usedTypeElements, nonPrimaryClassesToPrimaryClass); - } - } + String methodWithoutAnySpace = methodName.replaceAll("\\s", ""); + if (this.targetMethodNames.contains(methodWithoutAnySpace)) { - ResolvedMethodDeclaration resolvedMethod = method.resolve(); - updateUsedClassesForInterface(resolvedMethod); - updateUsedClassWithQualifiedClassName( - JavaParserUtil.packagePrefix(resolvedMethod) + resolvedMethod.getClassName(), - usedTypeElements, - nonPrimaryClassesToPrimaryClass); - - insideTargetMember = true; - targetMethods.add(resolvedMethod.getQualifiedSignature()); - // make sure that differences in spacing does not interfere with the result - for (String unfound : unfoundMethods.keySet()) { - if (unfound.replaceAll("\\s", "").equals(methodWithoutAnySpace)) { - unfoundMethods.remove(unfound); - break; - } - } - Type returnType = method.getType(); - // JavaParser may misinterpret unresolved array types as reference types. - // To ensure accuracy, we resolve the type before proceeding with the check. - try { - ResolvedType resolvedType = returnType.resolve(); - if (resolvedType instanceof ResolvedReferenceType) { - updateUsedClassBasedOnType(resolvedType); - } - } catch (UnsupportedOperationException e) { - // Occurs if the type is a type variable, so there is nothing to do: - // the type variable must have been declared in one of the containing scopes, - // and UnsolvedSymbolVisitor should already guarantee that the variable will - // be included in one of the classes that Specimin outputs. - } catch (UnsolvedSymbolException e) { - throw new RuntimeException( - "failed to solve the return type (" - + returnType - + ") of " - + methodWithoutReturnAndAnnos, - e); - } + unfoundMethods.remove(methodWithoutAnySpace); + addMethodAndChildrenToWorklist(method); } else { - updateUnfoundMethods(methodName); - } - - Visitable result = super.visit(method, p); - insideTargetMember = oldInsideTargetMember; - return result; - } - - @Override - public Visitable visit(Parameter para, Void p) { - if (insideTargetMember) { - Type type = para.getType(); - // an unknown type plays the role of a null object for lambda parameters that have no explicit - // type declared. However, we also want to avoid trying to solve declared lambda params (it - // will - // fail, despite the fact that the code should be compilable). - boolean isLambdaParam = type.isUnknownType() || isLambdaParam(para); - if (type.isUnionType()) { - resolveUnionType(type.asUnionType()); - } else if (!isLambdaParam) { - // Parameter resolution (para.resolve()) does not work in catch clause. - // However, resolution works on the type of the parameter. - // Bug report: https://github.com/javaparser/javaparser/issues/4240 - ResolvedType paramType; - if (para.getParentNode().isPresent() && para.getParentNode().get() instanceof CatchClause) { - paramType = para.getType().resolve(); - } else { - try { - paramType = para.resolve().getType(); - } catch (UnsupportedOperationException e) { - throw new RuntimeException("cannot solve: " + para, e); - } - } - - if (paramType.isReferenceType()) { - String paraTypeFullName = - paramType.asReferenceType().getTypeDeclaration().orElseThrow().getQualifiedName(); - updateUsedClassWithQualifiedClassName( - paraTypeFullName, usedTypeElements, nonPrimaryClassesToPrimaryClass); - for (ResolvedType typeParameterValue : - paramType.asReferenceType().typeParametersValues()) { - String typeParameterValueName = typeParameterValue.describe(); - if (typeParameterValueName.contains("<")) { - // removing the "<...>" part if there is any. - typeParameterValueName = - typeParameterValueName.substring(0, typeParameterValueName.indexOf("<")); - } - updateUsedClassWithQualifiedClassName( - typeParameterValueName, usedTypeElements, nonPrimaryClassesToPrimaryClass); - } - } - } + updateUnfoundMethods(methodWithoutAnySpace); } - return super.visit(para, p); + return super.visit(method, p); } /** - * Returns true iff we can prove that the parameter is a lambda parameter. This method should only - * be called on parameters that are not of unknown type (which are definitely lambda params). + * Adds a callable (constructor/method) declaration and its body to the worklist. * - * @param para a parameter - * @return true iff para is a (typed) parameter in a lambda + * @param method The method/constructor to add */ - private boolean isLambdaParam(Parameter para) { - return para.getParentNode().orElseThrow() instanceof LambdaExpr; - } + private void addMethodAndChildrenToWorklist(CallableDeclaration method) { + // Add itself to the worklist + worklist.add(method); - @Override - public Visitable visit(MethodReferenceExpr ref, Void p) { - if (insideTargetMember) { - ResolvedMethodDeclaration decl = ref.resolve(); - preserveMethodDecl(decl); - } - return super.visit(ref, p); - } + // Add body to the worklist - @Override - public Visitable visit(MethodCallExpr call, Void p) { - if (insideTargetMember) { - ResolvedMethodDeclaration decl; - try { - decl = call.resolve(); - } catch (UnsupportedOperationException e) { - // This case only occurs when a method is called on a lambda parameter. - // JavaParser has a type variable for the lambda parameter, but it won't - // have any constraints (JavaParser isn't very good at solving lambda parameter - // types). The approach here preserves any method that might be the callee that's - // in the input (based on the simple name of the method and its number of parameters). - // TODO: this approach is both unsound and imprecise but works most of the time on - // real examples. A better approach would be to either: - // * update to a new version of JavaParser that _can_ solve lambda parameters - // (we believe that newer JP versions are much improved), or - // * add another javac pass after pruning that checks for this kind of error. - resolvedYetStuckMethodCall.add(call.getNameAsString() + "@" + call.getArguments().size()); - return super.visit(call, p); - } catch (RuntimeException e) { - // Handle cases where a method call is resolved but its signature confuses JavaParser, - // leading to a RuntimeException. - // Note: this preservation is safe because we are not having an UnsolvedSymbolException. - // Only unsolved symbols can make the output failed to compile. - if (call.hasScope()) { - Expression scope = call.getScope().orElseThrow(); - String scopeAsString = scope.toString(); - if (scopeAsString.equals("this") || scopeAsString.equals("super")) { - // In the "super" case, it would be better to add the name of an - // extended or implemented class/interface. However, there are two complications: - // 1) we currently don't track the list of classes/interfaces that the current class - // extends and/or implements in this visitor and 2) even if we did track that, there - // is no way for us to know which of those classes/interfaces the method belongs to. - // TODO: write a test for the "super" case and then figure out a better way to handle - // it. - resolvedYetStuckMethodCall.add( - this.currentClassQualifiedName + "." + call.getNameAsString()); - } else { - // Use the scope instead. First, check if it's resolvable. If it is, great - - // just use that. If not, then we need to use some heuristics as fallbacks. - try { - ResolvedType scopeType = scope.calculateResolvedType(); - resolvedYetStuckMethodCall.add(scopeType.describe() + "." + call.getNameAsString()); - usedTypeElements.add(scopeType.describe()); - } catch (Exception e1) { - // There are two fallback cases: the scope is an FQN (e.g., in - // a call to a fully-qualified static method) or the scope is a simple name. - // In the simple name case, append the current package to the front, since - // if it had been imported we wouldn't be in this situation. - if (JavaParserUtil.isAClassPath(scopeAsString)) { - resolvedYetStuckMethodCall.add(scopeAsString + "." + call.getNameAsString()); - usedTypeElements.add(scopeAsString); - } else { - String packagePrefix = - getCurrentPackage().isEmpty() ? "" : getCurrentPackage() + "."; - resolvedYetStuckMethodCall.add( - packagePrefix + scopeAsString + "." + call.getNameAsString()); - usedTypeElements.add(packagePrefix + scopeAsString); - } - } - } - } else { - resolvedYetStuckMethodCall.add( - this.currentClassQualifiedName + "." + call.getNameAsString()); - } - return super.visit(call, p); - } - preserveMethodDecl(decl); - // Special case for lambdas/method references to preserve artificial functional - // interfaces. - for (int i = 0; i < call.getArguments().size(); ++i) { - Expression arg = call.getArgument(i); - if (arg.isLambdaExpr() || arg.isMethodReferenceExpr()) { - updateUsedClassBasedOnType(decl.getParam(i).getType()); - // We should mark the abstract method for preservation as well - if (decl.getParam(i).getType().isReferenceType()) { - ResolvedReferenceType functionalInterface = - decl.getParam(i).getType().asReferenceType(); - for (MethodUsage method : functionalInterface.getDeclaredMethods()) { - if (method.getDeclaration().isAbstract()) { - preserveMethodDecl(method.getDeclaration()); - // Only one abstract method per functional interface - break; - } - } - } - } - } - } - return super.visit(call, p); - } - - /** - * Helper method for preserving a used method. This code is called for both method call - * expressions and method refs. - * - * @param decl a resolved method declaration to be preserved - */ - private void preserveMethodDecl(ResolvedMethodDeclaration decl) { - usedMembers.add(decl.getQualifiedSignature()); - updateUsedClassWithQualifiedClassName( - JavaParserUtil.packagePrefix(decl) + decl.getClassName(), - usedTypeElements, - nonPrimaryClassesToPrimaryClass); - try { - ResolvedType methodReturnType = decl.getReturnType(); - if (methodReturnType instanceof ResolvedReferenceType) { - updateUsedClassBasedOnType(methodReturnType); - } - } - // There could be two cases here: - // 1) The return type is a completely generic type. - // 2) UnsolvedSymbolVisitor has missed some unsolved symbols. - catch (UnsolvedSymbolException e) { - return; - } - - for (int i = 0; i < decl.getNumberOfParams(); ++i) { - // Why is there no getParams() method?? - ResolvedParameterDeclaration p = decl.getParam(i); - ResolvedType pType = p.getType(); - updateUsedClassBasedOnType(pType); - } - } - - /** - * Gets the package name of the current class. - * - * @return the current package name - */ - private String getCurrentPackage() { - return currentPackage; - } - - @Override - public Visitable visit(ClassOrInterfaceType type, Void p) { - if (!insideTargetMember) { - return super.visit(type, p); - } - try { - ResolvedReferenceType typeResolved = - JavaParserUtil.classOrInterfaceTypeToResolvedReferenceType(type); - updateUsedClassBasedOnType(typeResolved); - } - // if the type has a fully-qualified form, JavaParser also consider other components rather than - // the class name as ClassOrInterfaceType. For example, if the type is org.A.B, then JavaParser - // will also consider org and org.A as ClassOrInterfaceType. - // if type is a type variable, we will get an UnsupportedOperation Exception. - catch (UnsolvedSymbolException | UnsupportedOperationException e) { - return super.visit(type, p); - } - return super.visit(type, p); - } - - @Override - public Visitable visit(ObjectCreationExpr newExpr, Void p) { - if (insideTargetMember) { - try { - ResolvedConstructorDeclaration resolved = newExpr.resolve(); - usedMembers.add(resolved.getQualifiedSignature()); - updateUsedClassWithQualifiedClassName( - JavaParserUtil.packagePrefix(resolved) + resolved.getClassName(), - usedTypeElements, - nonPrimaryClassesToPrimaryClass); - for (int i = 0; i < resolved.getNumberOfParams(); ++i) { - // Why is there no getParams() method?? - ResolvedParameterDeclaration param = resolved.getParam(i); - ResolvedType pType = param.getType(); - updateUsedClassBasedOnType(pType); - } - } catch (UnsolvedSymbolException e) { - throw new RuntimeException("trying to resolve : " + newExpr, e); - } - } - return super.visit(newExpr, p); - } - - @Override - public Visitable visit(ExplicitConstructorInvocationStmt expr, Void p) { - if (insideTargetMember) { - ResolvedConstructorDeclaration resolved = expr.resolve(); - usedMembers.add(resolved.getQualifiedSignature()); - updateUsedClassWithQualifiedClassName( - JavaParserUtil.packagePrefix(resolved) + resolved.getClassName(), - usedTypeElements, - nonPrimaryClassesToPrimaryClass); - } - return super.visit(expr, p); - } - - @Override - public Visitable visit(EnumConstantDeclaration enumConstantDeclaration, Void p) { - Node parentNode = enumConstantDeclaration.getParentNode().orElseThrow(); - - if (parentNode instanceof EnumDeclaration) { - if (usedTypeElements.contains( - ((EnumDeclaration) parentNode) - .asEnumDeclaration() - .getFullyQualifiedName() - .orElseThrow())) { - boolean oldInsideTargetMember = insideTargetMember; - // used enum constant are not strictly target methods, but we need to make sure the symbols - // inside them are preserved. - insideTargetMember = true; - Visitable result = super.visit(enumConstantDeclaration, p); - insideTargetMember = oldInsideTargetMember; - - return result; - } - } - return super.visit(enumConstantDeclaration, p); - } + if (method instanceof ConstructorDeclaration constructor) { + worklist.add(constructor.getBody()); + worklist.addAll(constructor.getBody().getStatements()); + } else { + Optional body = ((MethodDeclaration) method).getBody(); - @Override - public Visitable visit(FieldAccessExpr expr, Void p) { - if (insideTargetMember) { - String fullNameOfClass; - if (updateUsedClassAndMemberForEnumConstant(expr)) { - return super.visit(expr, p); + if (body.isPresent()) { + worklist.add(body.get()); } - try { - // while the name of the method is declaringType(), it actually returns the class where the - // field is declared - fullNameOfClass = expr.resolve().asField().declaringType().getQualifiedName(); - usedMembers.add(fullNameOfClass + "#" + expr.getName().asString()); - updateUsedClassWithQualifiedClassName( - fullNameOfClass, usedTypeElements, nonPrimaryClassesToPrimaryClass); - ResolvedType exprResolvedType = expr.resolve().getType(); - updateUsedClassBasedOnType(exprResolvedType); - } catch (UnsolvedSymbolException | UnsupportedOperationException e) { - // when the type is a primitive array, we will have an UnsupportedOperationException - if (e instanceof UnsupportedOperationException) { - Expression scope = expr.getScope(); - if (scope.isNameExpr()) { - updateUsedElementWithPotentialFieldNameExpr(scope.asNameExpr()); - } - // If the scope is not a name expression, then it must be "this" (handled elsewhere), - // "super" (handled directly below), or another field access expression (handled by - // the visitor), so there's nothing to do. - } - // if a field is accessed in the form of a fully-qualified path, such as - // org.example.A.b, then other components in the path apart from the class name and field - // name, such as org and org.example, will also be considered as FieldAccessExpr. - } - } - Expression caller = expr.getScope(); - if (caller instanceof SuperExpr) { - ResolvedType callerResolvedType = caller.calculateResolvedType(); - updateUsedClassBasedOnType(callerResolvedType); } - return super.visit(expr, p); } @Override - public Visitable visit(NameExpr expr, Void p) { - if (insideTargetMember) { - Optional parentNode = expr.getParentNode(); - if (parentNode.isEmpty() || !(parentNode.get() instanceof FieldAccessExpr)) { - updateUsedElementWithPotentialFieldNameExpr(expr); - } - } - return super.visit(expr, p); - } - - /** - * Updates the list of used classes based on a resolved method declaration. If the input method - * originates from an interface, that interface will be added to the list of used classes. The - * determination of whether a method belongs to an interface is based on three criteria: method - * name, method return type, and the number of parameters. - * - * @param method The resolved method declaration to be used for updating the list. - */ - public void updateUsedClassesForInterface(ResolvedMethodDeclaration method) { - for (ResolvedMethodDeclaration interfaceMethod : methodDeclarationToInterfaceType.keySet()) { - if (method.getName().equals(interfaceMethod.getName())) { - try { - if (method - .getReturnType() - .describe() - .equals(interfaceMethod.getReturnType().describe())) { - if (method.getNumberOfParams() == interfaceMethod.getNumberOfParams()) { - ResolvedReferenceType resolvedInterface = - JavaParserUtil.classOrInterfaceTypeToResolvedReferenceType( - methodDeclarationToInterfaceType.get(interfaceMethod)); - updateUsedClassWithQualifiedClassName( - resolvedInterface.getQualifiedName(), - usedTypeElements, - nonPrimaryClassesToPrimaryClass); - usedMembers.add(interfaceMethod.getQualifiedSignature()); - } - } - } catch (UnsolvedSymbolException e) { - // only potentially-used members will have their symbols solved. - continue; - } - } - } - } - - /** - * Resolves unionType parameters one by one and adds them in the usedClass set. - * - * @param type unionType parameter - */ - private void resolveUnionType(UnionType type) { - for (ReferenceType param : type.getElements()) { - ResolvedType paramType = param.resolve(); - updateUsedClassBasedOnType(paramType); - } - } - - /** - * Given a FieldAccessExpr, this method updates the sets of used classes and members if this field - * is actually an enum constant. - * - * @param fieldAccessExpr a potential enum constant. - * @return true if the updating process was successful, false otherwise. - */ - private boolean updateUsedClassAndMemberForEnumConstant(FieldAccessExpr fieldAccessExpr) { - ResolvedValueDeclaration resolved; - try { - resolved = fieldAccessExpr.resolve(); - } - // if the a field is accessed in the form of a fully-qualified path, such as - // org.example.A.b, then other components in the path apart from the class name and field - // name, such as org and org.example, will also be considered as FieldAccessExpr. - catch (UnsolvedSymbolException | UnsupportedOperationException e) { - return false; - } - if (!resolved.isEnumConstant()) { - return false; - } - String classFullName = resolved.asEnumConstant().getType().describe(); - updateUsedClassWithQualifiedClassName( - classFullName, usedTypeElements, nonPrimaryClassesToPrimaryClass); - usedMembers.add(classFullName + "." + fieldAccessExpr.getNameAsString()); - return true; - } - - /** - * Given a NameExpr instance, this method will update the used elements, classes and members if - * that NameExpr is a field. - * - * @param expr a field access expression inside target methods - */ - public void updateUsedElementWithPotentialFieldNameExpr(NameExpr expr) { - ResolvedValueDeclaration exprDecl; - try { - exprDecl = expr.resolve(); - } catch (UnsolvedSymbolException e) { - // if expr is the name of a class in a static call, we can't resolve its value. - return; - } - if (exprDecl instanceof ResolvedFieldDeclaration) { - // while the name of the method is declaringType(), it actually returns the class where the - // field is declared - String classFullName = exprDecl.asField().declaringType().getQualifiedName(); - updateUsedClassWithQualifiedClassName( - classFullName, usedTypeElements, nonPrimaryClassesToPrimaryClass); - usedMembers.add(classFullName + "#" + expr.getNameAsString()); - updateUsedClassBasedOnType(exprDecl.getType()); - } else if (exprDecl instanceof ResolvedEnumConstantDeclaration) { - String enumFullName = exprDecl.asEnumConstant().getType().describe(); - updateUsedClassWithQualifiedClassName( - enumFullName, usedTypeElements, nonPrimaryClassesToPrimaryClass); - // "." and not "#" because enum constants are not fields - usedMembers.add(enumFullName + "." + expr.getNameAsString()); - updateUsedClassBasedOnType(exprDecl.getType()); - } - } - - /** - * Updates the list of used type elements with the given qualified type name and its corresponding - * primary type and enclosing type. This includes cases such as classes not sharing the same name - * as their Java files or nested classes. - * - * @param qualifiedClassName The qualified class name to be included in the list of used type - * elements. - * @param usedTypeElement The set of used type elements to be updated. - * @param nonPrimaryClassesToPrimaryClass Map connecting non-primary classes to their - * corresponding primary classes. - */ - public static void updateUsedClassWithQualifiedClassName( - String qualifiedClassName, - Set usedTypeElement, - Map nonPrimaryClassesToPrimaryClass) { - - // strip type variables, if they're present - if (qualifiedClassName.contains("<")) { - qualifiedClassName = qualifiedClassName.substring(0, qualifiedClassName.indexOf("<")); - } - usedTypeElement.add(qualifiedClassName); - // in case this class is not a primary class. - if (nonPrimaryClassesToPrimaryClass.containsKey(qualifiedClassName)) { - updateUsedClassWithQualifiedClassName( - nonPrimaryClassesToPrimaryClass.get(qualifiedClassName), - usedTypeElement, - nonPrimaryClassesToPrimaryClass); - } - - // in case of type variables TODO:investigate side effects of having moved this from earlier - if (!qualifiedClassName.contains(".")) { - return; - } - String potentialOuterClass = - qualifiedClassName.substring(0, qualifiedClassName.lastIndexOf(".")); - if (JavaParserUtil.isAClassPath(potentialOuterClass)) { - updateUsedClassWithQualifiedClassName( - potentialOuterClass, usedTypeElement, nonPrimaryClassesToPrimaryClass); - } - } + public Visitable visit(VariableDeclarator node, Void arg) { + if (node.getParentNode().isPresent() + && node.getParentNode().get() instanceof FieldDeclaration) { + String fieldName = this.currentClassQualifiedName + "#" + node.getNameAsString(); + if (targetFields.contains(fieldName)) { + unfoundFields.remove(fieldName); - /** - * Updates the list of used classes based on the resolved type of a used element, where an element - * can be a method, a field, a variable, or a parameter. Also updates the set of used classes - * based on component types, wildcard bounds, etc., as needed: any type that is used in the type - * will be included. - * - * @param type The resolved type of the used element. - */ - public void updateUsedClassBasedOnType(ResolvedType type) { - if (type.isTypeVariable()) { - // From JLS 4.4: A type variable is introduced by the declaration of a type parameter of a - // generic class, interface, method, or constructor - ResolvedTypeParameterDeclaration asTypeParameter = type.asTypeParameter(); - for (ResolvedTypeParameterDeclaration.Bound bound : asTypeParameter.getBounds()) { - updateUsedClassWithQualifiedClassName( - bound.getType().describe(), usedTypeElements, nonPrimaryClassesToPrimaryClass); - } - return; - } else if (type.isArray()) { - ResolvedType componentType = type.asArrayType().getComponentType(); - updateUsedClassBasedOnType(componentType); - return; - } - updateUsedClassWithQualifiedClassName( - type.describe(), usedTypeElements, nonPrimaryClassesToPrimaryClass); - if (!type.isReferenceType()) { - return; - } - ResolvedReferenceType typeAsReference = type.asReferenceType(); - List typeParameters = typeAsReference.typeParametersValues(); - for (ResolvedType typePara : typeParameters) { - if (typePara.isPrimitive() || typePara.isTypeVariable()) { - // Nothing to do, since these are already in-scope. - continue; - } - if (typePara.isWildcard()) { - ResolvedWildcard asWildcard = typePara.asWildcard(); - // Recurse into the bound, if one exists. - if (asWildcard.isBounded()) { - updateUsedClassBasedOnType(asWildcard.getBoundedType()); + worklist.add(node); + if (node.getInitializer().isPresent()) { + worklist.add(node.getInitializer().get()); } - continue; - } - if (typePara.isArray()) { - ResolvedType componentType = typePara.asArrayType().getComponentType(); - updateUsedClassBasedOnType(componentType); - continue; + } else { + updateUnfoundFields(fieldName); } - updateUsedClassWithQualifiedClassName( - typePara.asReferenceType().getQualifiedName(), - usedTypeElements, - nonPrimaryClassesToPrimaryClass); } + return super.visit(node, arg); } } diff --git a/src/main/java/org/checkerframework/specimin/TypeRuleDependencyMap.java b/src/main/java/org/checkerframework/specimin/TypeRuleDependencyMap.java new file mode 100644 index 000000000..6d2db73c3 --- /dev/null +++ b/src/main/java/org/checkerframework/specimin/TypeRuleDependencyMap.java @@ -0,0 +1,28 @@ +package org.checkerframework.specimin; + +import com.github.javaparser.ast.Node; +import java.util.List; + +/** + * This class provides a method to help determine what other elements are relevant when processing + * an element. + */ +public interface TypeRuleDependencyMap { + /** + * Given a node, return all relevant nodes based on its type. + * + * @param node The node + * @return All relevant nodes to the input node. For example, this could be annotations, type + * parameters, parameters, return type, etc. for methods. + */ + public List getRelevantElements(Node node); + + /** + * Given a resolved object, return all relevant nodes. For example, a resolved method declaration + * would return the method declaration and its declaring type, both as attached nodes. + * + * @param resolved The resolved object + * @return All relevant nodes to the input resolved object. + */ + public List getRelevantElements(Object resolved); +} diff --git a/src/main/java/org/checkerframework/specimin/UnsolvedAnnotationRemoverVisitor.java b/src/main/java/org/checkerframework/specimin/UnsolvedAnnotationRemoverVisitor.java deleted file mode 100644 index 7224d4db9..000000000 --- a/src/main/java/org/checkerframework/specimin/UnsolvedAnnotationRemoverVisitor.java +++ /dev/null @@ -1,132 +0,0 @@ -package org.checkerframework.specimin; - -import com.github.javaparser.ast.ImportDeclaration; -import com.github.javaparser.ast.Node; -import com.github.javaparser.ast.expr.AnnotationExpr; -import com.github.javaparser.ast.expr.MarkerAnnotationExpr; -import com.github.javaparser.ast.expr.NormalAnnotationExpr; -import com.github.javaparser.ast.expr.SingleMemberAnnotationExpr; -import com.github.javaparser.ast.visitor.ModifierVisitor; -import com.github.javaparser.ast.visitor.Visitable; -import com.github.javaparser.resolution.UnsolvedSymbolException; -import com.github.javaparser.resolution.declarations.ResolvedAnnotationDeclaration; -import com.github.javaparser.symbolsolver.reflectionmodel.ReflectionAnnotationDeclaration; -import com.github.javaparser.symbolsolver.resolution.typesolvers.JarTypeSolver; -import java.io.IOException; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -/** A visitor that removes unsolved annotation expressions. */ -public class UnsolvedAnnotationRemoverVisitor extends ModifierVisitor { - /** - * List of paths of jar files to be used as input. Note: this is the set of every jar path, not - * just the jar paths used by the current compilation unit. - */ - List jarPaths; - - /** Map every class in the set of jar files to the corresponding jar file */ - Map classToJarPath = new HashMap<>(); - - /** - * Map a class to its fully qualified name based on the import statements of the current - * compilation unit. - */ - Map classToFullClassName = new HashMap<>(); - - /** - * Create a new instance of UnsolvedAnnotationRemoverVisitor - * - * @param jarPaths a list of paths of jar files to be used as input - */ - public UnsolvedAnnotationRemoverVisitor(List jarPaths) { - this.jarPaths = jarPaths; - for (String jarPath : jarPaths) { - try { - JarTypeSolver jarSolver = new JarTypeSolver(jarPath); - for (String fullClassName : jarSolver.getKnownClasses()) { - classToJarPath.put(fullClassName, jarPath); - } - } catch (IOException e) { - throw new RuntimeException(e); - } - } - } - - @Override - public Node visit(ImportDeclaration decl, Void p) { - String classFullName = decl.getNameAsString(); - String className = classFullName.substring(classFullName.lastIndexOf(".") + 1); - classToFullClassName.put(className, classFullName); - return decl; - } - - @Override - public Visitable visit(MarkerAnnotationExpr expr, Void p) { - processAnnotations(expr); - return super.visit(expr, p); - } - - @Override - public Visitable visit(NormalAnnotationExpr expr, Void p) { - processAnnotations(expr); - return super.visit(expr, p); - } - - @Override - public Visitable visit(SingleMemberAnnotationExpr expr, Void p) { - processAnnotations(expr); - return super.visit(expr, p); - } - - /** - * Processes annotations by removing annotations that are not solvable by the input list of jar - * files. - * - * @param annotation the annotation to be processed - */ - public void processAnnotations(AnnotationExpr annotation) { - String annotationName = annotation.getNameAsString(); - - // Never preserve @Override, since it causes compile errors but does not fix them. - if ("Override".equals(annotationName)) { - annotation.remove(); - return; - } - - // If the annotation can be resolved, find its qualified name to prevent removal - boolean isResolved = true; - try { - ResolvedAnnotationDeclaration resolvedAnno = annotation.resolve(); - annotationName = resolvedAnno.getQualifiedName(); - - if (resolvedAnno instanceof ReflectionAnnotationDeclaration) { - // These annotations do not have a file corresponding to them, which can cause - // compile errors in the output - // This is fine if it's included in java.lang, but if not, we should treat it as - // if it were unresolved - - if (!JavaLangUtils.inJdkPackage(annotationName)) { - isResolved = false; - } - } - } catch (UnsolvedSymbolException ex) { - isResolved = false; - } - - if (!JavaParserUtil.isAClassPath(annotationName)) { - if (!classToFullClassName.containsKey(annotationName)) { - // An annotation not imported and from the java.lang package is not our concern. - if (!JavaLangUtils.isJavaLangName(annotationName)) { - annotation.remove(); - } - return; - } - annotationName = classToFullClassName.get(annotationName); - } - - if (!isResolved && !classToJarPath.containsKey(annotationName)) { - annotation.remove(); - } - } -} diff --git a/src/main/java/org/checkerframework/specimin/UnsolvedClassOrInterface.java b/src/main/java/org/checkerframework/specimin/UnsolvedClassOrInterface.java deleted file mode 100644 index 8a1c3428d..000000000 --- a/src/main/java/org/checkerframework/specimin/UnsolvedClassOrInterface.java +++ /dev/null @@ -1,595 +0,0 @@ -package org.checkerframework.specimin; - -import com.google.common.base.Splitter; -import java.util.HashSet; -import java.util.Iterator; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Objects; -import java.util.Set; -import org.checkerframework.checker.nullness.qual.MonotonicNonNull; -import org.checkerframework.checker.nullness.qual.Nullable; -import org.checkerframework.checker.signature.qual.ClassGetSimpleName; - -/** - * An UnsolvedClassOrInterface instance is a representation of a class or an interface that can not - * be solved by SymbolSolver. The reason is that the class file is not in the root directory. - */ -public class UnsolvedClassOrInterface { - /** - * Set of methods belongs to the class. Must be a linked set to ensure deterministic iteration - * order when writing files synthetic classes. - */ - private final LinkedHashSet methods; - - /** The name of the class */ - private final @ClassGetSimpleName String className; - - /** - * The fields of this class. Must be a linked set to ensure deterministic iteration order when - * writing files for synthetic classes. - */ - private final LinkedHashSet classFields; - - /** - * The name of the package of the class. We rely on the import statements from the source codes to - * guess the package name. - */ - private final String packageName; - - /** This field records the number of type variables for this class */ - private int numberOfTypeVariables = 0; - - /** This field records the name of type variables that we prefer this class to have. */ - private Set preferredTypeVariables = new HashSet<>(); - - /** This field records the extends clause, if one exists. */ - private @Nullable String extendsClause; - - /** The implements clauses, if they exist. */ - private Set implementsClauses = new LinkedHashSet<>(0); - - /** This field records if the class is an interface */ - private boolean isAnInterface; - - /** This class' inner classes. */ - private @MonotonicNonNull Set innerClasses = null; - - /** Is this class an annotation? */ - private boolean isAnAnnotation = false; - - /** - * This class' constructor should be used for creating inner classes. Frankly, this design is a - * mess (sorry) - controlling whether this is an inner class via inheritance is probably bad. - * TODO: clean this up after ISSTA. - */ - public static class UnsolvedInnerClass extends UnsolvedClassOrInterface { - /** - * Create an instance of UnsolvedInnerClass. - * - * @param className the name of the inner class, possibly followed by a set of type arguments - * @param packageName the name of the package containing the outer class - */ - public UnsolvedInnerClass(String className, String packageName) { - super(className, packageName); - } - } - - /** - * Create an instance of UnsolvedClass. This constructor correctly splits apart the class name and - * any generics attached to it. - * - * @param className the name of the class, possibly followed by a set of type arguments - * @param packageName the name of the package - */ - public UnsolvedClassOrInterface(String className, String packageName) { - this(className, packageName, false); - } - - /** - * Create an instance of UnsolvedClass - * - * @param className the simple name of the class, possibly followed by a set of type arguments - * @param packageName the name of the package - * @param isException does the class represents an exception? - */ - public UnsolvedClassOrInterface(String className, String packageName, boolean isException) { - this(className, packageName, isException, false); - } - - /** - * Create an instance of an unsolved interface or unsolved class. - * - * @param className the simple name of the interface, possibly followed by a set of type arguments - * @param packageName the name of the package - * @param isException does the interface represents an exception? - * @param isAnInterface check whether this is an interface or a class - */ - public UnsolvedClassOrInterface( - String className, String packageName, boolean isException, boolean isAnInterface) { - if (className.contains("<")) { - @SuppressWarnings("signature") // removing the <> makes this a true simple name - @ClassGetSimpleName String classNameWithoutAngleBrackets = className.substring(0, className.indexOf('<')); - this.className = classNameWithoutAngleBrackets; - } else { - @SuppressWarnings("signature") // no angle brackets means this is a true simple name - @ClassGetSimpleName String classNameWithoutAngleBrackets = className; - this.className = classNameWithoutAngleBrackets; - } - this.methods = new LinkedHashSet<>(); - this.packageName = packageName; - this.classFields = new LinkedHashSet<>(); - if (isException) { - this.extendsClause = " extends Exception"; - } - this.isAnInterface = isAnInterface; - } - - /** - * Returns the value of isAnInterface. - * - * @return return true if the current UnsolvedClassOrInterface instance represents an interface. - */ - public boolean isAnInterface() { - return isAnInterface; - } - - /** - * Sets isAnInterface to true. isAnInterface is monotonic: it can start as false and become true - * (because we encounter an implements clause), but it can never go from true to false. - */ - public void setIsAnInterfaceToTrue() { - this.isAnInterface = true; - } - - /** - * Sets isAnAnnotation to true. isAnAnnotation is monotonic: it can start as false and become true - * (because we encounter evidence that this is an annotation), but it can never go from true to - * false. - */ - public void setIsAnAnnotationToTrue() { - this.isAnAnnotation = true; - } - - /** - * Get the list of methods from this synthetic class - * - * @return the list of methods - */ - public Set getMethods() { - return methods; - } - - /** - * Get the name of this class (note: without any generic type variables). - * - * @return the name of the class - */ - public @ClassGetSimpleName String getClassName() { - return className; - } - - /** - * Return the qualified name of this class. - * - * @return the qualified name - */ - public String getQualifiedClassName() { - return packageName + "." + className; - } - - /** - * Get the package where this class belongs to - * - * @return the value of packageName - */ - public String getPackageName() { - return packageName; - } - - /** - * Get the fields of this current class - * - * @return classVariables - */ - public Set getClassFields() { - return classFields; - } - - /** - * Add a method to the class - * - * @param method the method to be added - */ - public void addMethod(UnsolvedMethod method) { - // Check for another method with the same parameter list, but with differences in - // whether the parameter names are fully-qualified or simple names. - List paramList = method.getParameterList(); - UnsolvedMethod matchingMethod = null; - boolean preferOther = true; - methods: - for (UnsolvedMethod otherMethod : this.methods) { - // Skip methods that are definitely different. - if (!method.getName().equals(otherMethod.getName()) - || otherMethod.getParameterList().size() != paramList.size()) { - continue; - } - - for (int i = 0; i < paramList.size(); ++i) { - String paramType = paramList.get(i); - String otherParamType = otherMethod.getParameterList().get(i); - if ((this.packageName + "." + otherParamType).equals(paramType)) { - // In this case, the current method has the FQNs. - preferOther = false; - } else if (otherParamType.equals(this.packageName + "." + paramType)) { - // The other method already has the FQNs, so do nothing here. - } else { - // if there is ever a difference, skip to the next method; there is - // no need to check for exact equality, because methods is a set - // so any duplicates won't be added. - continue methods; - } - } - matchingMethod = otherMethod; - } - - if (matchingMethod != null) { - if (preferOther) { - // Nothing more to do: the correct method is already present, - // and this one is a duplicate with simple names. - return; - } else { - // We need to replace the current variant of the method with this one. - // So, remove the current one (the add call below will take care of - // adding this method, just as if this was a totally new method). - this.methods.remove(matchingMethod); - } - } - - this.methods.add(method); - } - - /** - * Add field declaration to the class. We expect something like "int i" or "String y" instead of - * just "i" and "y" - * - * @param variableExpression the expression of the variables to be added - */ - public void addFields(String variableExpression) { - this.classFields.add(variableExpression); - } - - /** - * This method sets the number of type variables for the current class - * - * @param numberOfTypeVariables number of type variable in this class. - */ - public void setNumberOfTypeVariables(int numberOfTypeVariables) { - this.numberOfTypeVariables = numberOfTypeVariables; - } - - /** - * Set the value for preferredTypeVariables. - * - * @param preferredTypeVariables desired value for preferredTypeVariables. - */ - public void setPreferedTypeVariables(Set preferredTypeVariables) { - this.preferredTypeVariables = preferredTypeVariables; - } - - /** - * This method tells the number of type variables for this class - * - * @return the number of type variables - */ - public int getNumberOfTypeVariables() { - return this.numberOfTypeVariables; - } - - /** - * Adds a new interface to the list of implemented interfaces. - * - * @param interfaceName the fqn of the interface - */ - public void implement(String interfaceName) { - implementsClauses.add(interfaceName); - } - - /** - * Adds an extends clause to this class. - * - * @param className a fully-qualified class name for the class to be extended - */ - public void extend(String className) { - this.extendsClause = "extends " + className; - } - - /** - * Attempts to add an extends clause to this class or (recursively) to one of its inner classes. - * An extends clause will only be added if the name of this class matches the target type name. - * The name of the class in the extends clause is extendsName. - * - * @param targetTypeName the name of the class to be extended. This may be a fully-qualified name, - * a simple name, or a dot-separated identifier. - * @param extendsName the name of the class to extend. Always fully-qualified. - * @param visitor the current visitor state - * @return true if an extends clause was added, false otherwise - */ - public boolean extend(String targetTypeName, String extendsName, UnsolvedSymbolVisitor visitor) { - if (targetTypeName.equals(this.getQualifiedClassName()) - || targetTypeName.equals(this.getClassName())) { - // Special case: if the type to extend is "Annotation", then change the - // target class to an @interface declaration. - if ("Annotation".equals(extendsName) - || "java.lang.annotation.Annotation".equals(extendsName)) { - setIsAnAnnotationToTrue(); - } else { - if (!JavaParserUtil.isAClassPath(extendsName)) { - extendsName = visitor.getPackageFromClassName(extendsName) + "." + extendsName; - } - extend(extendsName); - } - return true; - } - if (innerClasses == null) { - return false; - } - // Two possibilities, depending on how Javac's error message looks: - // 1. Javac provides the whole class name in the form Outer.Inner - // 2. Javac provides only the inner class name - if (targetTypeName.indexOf('.') != -1) { - String outerName = targetTypeName.substring(0, targetTypeName.indexOf('.')); - if (!outerName.equals(this.className)) { - return false; - } - // set the targetTypeName to the name of the inner class - targetTypeName = targetTypeName.substring(targetTypeName.indexOf('.') + 1); - } - boolean result = false; - for (UnsolvedClassOrInterface unsolvedInnerClass : innerClasses) { - result |= unsolvedInnerClass.extend(targetTypeName, extendsName, visitor); - } - return result; - } - - /** - * Update the return type of a method. Note: this method is supposed to be used to update - * synthetic methods, where the return type of each method is distinct. - * - * @param currentReturnType the current return type of this method - * @param desiredReturnType the new return type - * @return true if a type is successfully updated - */ - public boolean updateMethodByReturnType(String currentReturnType, String desiredReturnType) { - boolean successfullyUpdated = false; - for (UnsolvedMethod method : methods) { - if (method.getReturnType().equals(currentReturnType)) { - method.setReturnType(desiredReturnType); - successfullyUpdated = true; - } - } - return successfullyUpdated; - } - - /** - * This method updates the types of fields in this class - * - * @param currentType the current type - * @param correctType the desired type - * @return true if a type is successfully updated. - */ - public boolean updateFieldByType(String currentType, String correctType) { - boolean successfullyUpdated = false; - Iterator iterator = classFields.iterator(); - Set newFields = new HashSet<>(); - while (iterator.hasNext()) { - String fieldDeclared = iterator.next(); - String staticKeyword = ""; - String finalKeyword = ""; - // since these are fields in synthetic classes created by UnsolvedSymbolVisitor, if this field - // is both static and final, the static keyword will be placed before the final keyword. - if (fieldDeclared.startsWith("static")) { - fieldDeclared = fieldDeclared.replace("static ", ""); - staticKeyword = "static "; - } - if (fieldDeclared.startsWith("final")) { - fieldDeclared = fieldDeclared.replace("final ", ""); - finalKeyword = "final "; - } - List elements = Splitter.on(' ').splitToList(fieldDeclared); - // fieldExpression is guaranteed to have the form "TYPE FIELD_NAME". Since this field - // expression is from a synthetic class, there is no annotation involved, so TYPE has no - // space. - String fieldType = elements.get(0); - String fieldName = elements.get(1); - // endsWith here is important, because the output of javac (i.e., what it prints in the error - // message, which turns into currentType) is always a simple name, but fields in superclasses - // are output using FQNs - if (fieldType.endsWith(currentType)) { - successfullyUpdated = true; - iterator.remove(); - newFields.add( - UnsolvedSymbolVisitor.setInitialValueForVariableDeclaration( - correctType, staticKeyword + finalKeyword + correctType + " " + fieldName)); - } - } - - classFields.addAll(newFields); - return successfullyUpdated; - } - - /** - * Add the given class as an inner class to this class. - * - * @param innerClass the inner class to add - */ - public void addInnerClass(UnsolvedClassOrInterface innerClass) { - if (this.innerClasses == null) { - // LinkedHashSet to make the iteration order deterministic. - this.innerClasses = new LinkedHashSet<>(1); - } - this.innerClasses.add(innerClass); - } - - @Override - public boolean equals(@Nullable Object other) { - if (!(other instanceof UnsolvedClassOrInterface)) { - return false; - } - UnsolvedClassOrInterface otherClass = (UnsolvedClassOrInterface) other; - // Note: an UnsovledClass cannot represent an anonymous class - // (each UnsovledClass corresponds to a source file), so this - // check is sufficient for equality (it is checking the canonical name). - return otherClass.className.equals(this.className) - && otherClass.packageName.equals(this.packageName); - } - - @Override - public int hashCode() { - return Objects.hash(className, packageName); - } - - /** - * Return the content of the class as a compilable Java file. - * - * @return the content of the class - */ - @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - // TODO: this test is very, very bad practice and makes this class - // not reusable. Find a better way to do this after ISSTA. - if (this.getClass() != UnsolvedInnerClass.class) { - sb.append("package ").append(packageName).append(";\n"); - } - - // Synthetic annotations used within generic types cause compile errors, - // so we need to add this to prevent them - if (isAnAnnotation) { - sb.append( - "@java.lang.annotation.Target({ \n" - + "\tjava.lang.annotation.ElementType.TYPE, \n" - + "\tjava.lang.annotation.ElementType.FIELD, \n" - + "\tjava.lang.annotation.ElementType.METHOD, \n" - + "\tjava.lang.annotation.ElementType.PARAMETER, \n" - + "\tjava.lang.annotation.ElementType.CONSTRUCTOR, \n" - + "\tjava.lang.annotation.ElementType.LOCAL_VARIABLE, \n" - + "\tjava.lang.annotation.ElementType.ANNOTATION_TYPE,\n" - + "\tjava.lang.annotation.ElementType.PACKAGE,\n" - + "\tjava.lang.annotation.ElementType.TYPE_PARAMETER,\n" - + "\tjava.lang.annotation.ElementType.TYPE_USE \n" - + "})"); - } - - sb.append("public "); - if (this.getClass() == UnsolvedInnerClass.class) { - // Nested classes that are visible outside their parent class - // are usually static. There is no downside to making them static - // (it imposes no additional requirements), but there is a downside - // to making them non-static (they must be attached to a specific member - // of the outer class, which may or may not be true in the event). - // TODO: I'm not sure we actually have test cases for "real" inner classes - // (which are non-static nested classes). All of our "inner class" tests - // appear to be intended for static nested classes. See - // https://docs.oracle.com/javase/tutorial/java/javaOO/nested.html for - // a discussion of the difference. - sb.append("static "); - } - if (isAnInterface) { - // For synthetic interfaces created for lambdas only. - if (methods.size() == 1 - && (className.startsWith("SyntheticFunction") - || className.startsWith("SyntheticConsumer"))) { - sb.append("@FunctionalInterface\n"); - } - sb.append("interface "); - } else if (isAnAnnotation) { - sb.append("@interface "); - } else { - sb.append("class "); - } - sb.append(className).append(getTypeVariablesAsString()); - if (extendsClause != null) { - sb.append(" ").append(extendsClause); - } - if (!implementsClauses.isEmpty()) { - if (extendsClause != null) { - sb.append(", "); - } - sb.append(" implements "); - Iterator interfaces = implementsClauses.iterator(); - while (interfaces.hasNext()) { - sb.append(interfaces.next()); - if (interfaces.hasNext()) { - sb.append(", "); - } - } - } - sb.append(" {\n"); - if (innerClasses != null) { - for (UnsolvedClassOrInterface innerClass : innerClasses) { - sb.append(innerClass.toString()); - } - } - for (String variableDeclarations : classFields) { - sb.append(" " + "public ").append(variableDeclarations).append(";\n"); - } - for (UnsolvedMethod method : methods) { - sb.append(method.toString()); - } - sb.append("}\n"); - return sb.toString(); - } - - /** - * Return a synthetic representation for type variables of the current class. - * - * @return the synthetic representation for type variables - */ - public String getTypeVariablesAsString() { - if (numberOfTypeVariables == 0) { - return ""; - } - StringBuilder result = new StringBuilder(); - // if class A has three type variables, the expression will be A - result.append("<"); - getTypeVariablesImpl(result); - result.append(">"); - return result.toString(); - } - - /** - * Return a synthetic representation for type variables of the current class, without surrounding - * angle brackets. - * - * @return the synthetic representation for type variables - */ - public String getTypeVariablesAsStringWithoutBrackets() { - if (numberOfTypeVariables == 0) { - return ""; - } - StringBuilder result = new StringBuilder(); - getTypeVariablesImpl(result); - return result.toString(); - } - - /** - * Helper method for {@link #getTypeVariablesAsStringWithoutBrackets} and {@link - * #getTypeVariablesAsString()}. - * - * @param result a string builder. Will be side-effected. - */ - private void getTypeVariablesImpl(StringBuilder result) { - if (preferredTypeVariables.isEmpty()) { - for (int i = 0; i < numberOfTypeVariables; i++) { - String typeExpression = "T" + ((i > 0) ? i : ""); - result.append(typeExpression).append(", "); - } - } else { - for (String preferedTypeVar : preferredTypeVariables) { - result.append(preferedTypeVar).append(", "); - } - } - result.delete(result.length() - 2, result.length()); - } -} diff --git a/src/main/java/org/checkerframework/specimin/UnsolvedMethod.java b/src/main/java/org/checkerframework/specimin/UnsolvedMethod.java deleted file mode 100644 index dffc68521..000000000 --- a/src/main/java/org/checkerframework/specimin/UnsolvedMethod.java +++ /dev/null @@ -1,266 +0,0 @@ -package org.checkerframework.specimin; - -import java.util.Collections; -import java.util.List; -import java.util.Objects; -import org.checkerframework.checker.nullness.qual.Nullable; - -/** - * An UnsolvedMethod instance is a representation of a method that can not be solved by - * SymbolSolver. The reason is that the class file of that method is not in the root directory. - */ -public class UnsolvedMethod { - - /** The close() method from java.lang.AutoCloseable. */ - public static final UnsolvedMethod CLOSE = - new UnsolvedMethod( - "close", - "void", - Collections.emptyList(), - false, - "public", - List.of("java.lang.Exception")); - - /** The name of the method */ - private final String name; - - /** - * The return type of the method. At the moment, we set the return type the same as the class - * where the method belongs to. - */ - private String returnType; - - /** - * The list of the types of the parameters of the method. (Right now we won't touch it until the - * new variant of SymbolSolver is available) - */ - private final List parameterList; - - /** This field is set to true if this method is a static method */ - private boolean isStatic = false; - - /** - * Indicates whether this instance of UnsolvedMethod represents just a method signature without a - * body. - */ - private final boolean isJustMethodSignature; - - /** Access modifer of the current method. The value is set to "public" by default. */ - private final String accessModifier; - - /** The list of the types of the exceptions thrown by the method. */ - private final List throwsList; - - /** - * Create an instance of UnsolvedMethod - * - * @param name the name of the method - * @param returnType the return type of the method - * @param parameterList the list of parameters for this method - */ - public UnsolvedMethod(String name, String returnType, List parameterList) { - this(name, returnType, parameterList, false); - } - - /** - * Create an instance of UnsolvedMethod for a synthetic interface. - * - * @param name the name of the method - * @param returnType the return type of the method - * @param parameterList the list of parameters for this method - * @param isJustMethodSignature indicates whether this method represents just a method signature - * without a body - */ - public UnsolvedMethod( - String name, String returnType, List parameterList, boolean isJustMethodSignature) { - this(name, returnType, parameterList, isJustMethodSignature, "public"); - } - - /** - * Create an instance of UnsolvedMethod for a synthetic interface. - * - * @param name the name of the method - * @param returnType the return type of the method - * @param parameterList the list of parameters for this method - * @param isJustMethodSignature indicates whether this method represents just a method signature - * without a body - * @param accessModifier the access modifier of the current method - */ - public UnsolvedMethod( - String name, - String returnType, - List parameterList, - boolean isJustMethodSignature, - String accessModifier) { - this( - name, - returnType, - parameterList, - isJustMethodSignature, - accessModifier, - Collections.emptyList()); - } - - /** - * Create an instance of UnsolvedMethod for a synthetic interface. - * - * @param name the name of the method - * @param returnType the return type of the method - * @param parameterList the list of parameters for this method - * @param isJustMethodSignature indicates whether this method represents just a method signature - * without a body - * @param accessModifier the access modifier of the current method - * @param throwsList the list of exceptions thrown by this method - */ - public UnsolvedMethod( - String name, - String returnType, - List parameterList, - boolean isJustMethodSignature, - String accessModifier, - List throwsList) { - this.name = name; - this.returnType = returnType; - this.parameterList = parameterList; - this.isJustMethodSignature = isJustMethodSignature; - this.accessModifier = accessModifier; - this.throwsList = throwsList; - } - - /** - * Set the value of returnType. This method is used when javac tells us that UnsolvedSymbolVisitor - * get the return types wrong. - * - * @param returnType the return type to bet set for this method - */ - public void setReturnType(String returnType) { - this.returnType = returnType; - } - - /** - * Get the return type of this method - * - * @return the value of returnType - */ - public String getReturnType() { - return returnType; - } - - /** - * Get the name of this method - * - * @return the name of this method - */ - public String getName() { - return name; - } - - /** - * Getter for the parameter list. Note that the list is read-only. - * - * @return the parameter list - */ - public List getParameterList() { - return Collections.unmodifiableList(parameterList); - } - - /** - * Attempts to replace any parameters with the given name with java.lang.Object. If a parameter is - * successfully replaced, returns true. Otherwise, returns false. - * - * @param incorrectTypeName the type name to replace - * @return true if the name was replaced, false if not - */ - public boolean replaceParamWithObject(String incorrectTypeName) { - boolean result = false; - for (int i = 0; i < parameterList.size(); i++) { - if (parameterList.get(i).equals(incorrectTypeName)) { - parameterList.set(i, "java.lang.Object"); - result = true; - } - } - return result; - } - - /** - * Corrects the parameter's type at index {@code parameter} - * - * @param parameter The parameter (index) to replace - * @param correctName The type name to replace the parameter type as - */ - public void correctParameterType(int parameter, String correctName) { - parameterList.set(parameter, correctName); - } - - /** Set isStatic to true */ - public void setStatic() { - isStatic = true; - } - - @Override - public boolean equals(@Nullable Object o) { - if (!(o instanceof UnsolvedMethod)) { - return false; - } - UnsolvedMethod other = (UnsolvedMethod) o; - // This set of fields is based on the JLS' overloading rules. According to the documentation of - // Oracle: "You cannot declare more than one method with the same name and the same number and - // type of arguments, because the compiler cannot tell them apart. The compiler does not - // consider return type when differentiating methods, so you cannot declare two methods with the - // same signature even if they have a different return type." - return other.name.equals(this.name) && other.parameterList.equals(parameterList); - } - - @Override - public int hashCode() { - return Objects.hash(name, parameterList); - } - - /** - * Return the content of the method. Note that the body of the method is stubbed out. - * - * @return the content of the method with the body stubbed out - */ - @Override - public String toString() { - StringBuilder arguments = new StringBuilder(); - for (int i = 0; i < parameterList.size(); i++) { - String parameter = parameterList.get(i); - arguments.append(parameter).append(" ").append("parameter").append(i); - if (i < parameterList.size() - 1) { - arguments.append(", "); - } - } - StringBuilder signature = new StringBuilder(); - signature.append(accessModifier).append(" "); - if (isStatic) { - signature.append("static "); - } - if (!"".equals(returnType)) { - signature.append(returnType).append(" "); - } - signature.append(name).append("("); - signature.append(arguments); - signature.append(")"); - - if (!throwsList.isEmpty()) { - signature.append(" throws "); - } - - StringBuilder exceptions = new StringBuilder(); - for (int i = 0; i < throwsList.size(); i++) { - String exception = throwsList.get(i); - exceptions.append(exception); - if (i < parameterList.size() - 1) { - arguments.append(", "); - } - } - signature.append(exceptions); - - if (isJustMethodSignature) { - return signature.append(";").toString(); - } else { - return "\n " + signature + " {\n throw new java.lang.Error();\n }\n"; - } - } -} diff --git a/src/main/java/org/checkerframework/specimin/UnsolvedSymbolVisitor.java b/src/main/java/org/checkerframework/specimin/UnsolvedSymbolVisitor.java deleted file mode 100644 index 053106e76..000000000 --- a/src/main/java/org/checkerframework/specimin/UnsolvedSymbolVisitor.java +++ /dev/null @@ -1,4060 +0,0 @@ -package org.checkerframework.specimin; - -import com.github.javaparser.StaticJavaParser; -import com.github.javaparser.ast.AccessSpecifier; -import com.github.javaparser.ast.ImportDeclaration; -import com.github.javaparser.ast.Node; -import com.github.javaparser.ast.NodeList; -import com.github.javaparser.ast.PackageDeclaration; -import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration; -import com.github.javaparser.ast.body.ConstructorDeclaration; -import com.github.javaparser.ast.body.EnumConstantDeclaration; -import com.github.javaparser.ast.body.EnumDeclaration; -import com.github.javaparser.ast.body.FieldDeclaration; -import com.github.javaparser.ast.body.MethodDeclaration; -import com.github.javaparser.ast.body.Parameter; -import com.github.javaparser.ast.body.TypeDeclaration; -import com.github.javaparser.ast.body.VariableDeclarator; -import com.github.javaparser.ast.comments.Comment; -import com.github.javaparser.ast.expr.AnnotationExpr; -import com.github.javaparser.ast.expr.Expression; -import com.github.javaparser.ast.expr.FieldAccessExpr; -import com.github.javaparser.ast.expr.InstanceOfExpr; -import com.github.javaparser.ast.expr.LambdaExpr; -import com.github.javaparser.ast.expr.MarkerAnnotationExpr; -import com.github.javaparser.ast.expr.MemberValuePair; -import com.github.javaparser.ast.expr.MethodCallExpr; -import com.github.javaparser.ast.expr.MethodReferenceExpr; -import com.github.javaparser.ast.expr.NameExpr; -import com.github.javaparser.ast.expr.NormalAnnotationExpr; -import com.github.javaparser.ast.expr.ObjectCreationExpr; -import com.github.javaparser.ast.expr.PatternExpr; -import com.github.javaparser.ast.expr.SimpleName; -import com.github.javaparser.ast.expr.SingleMemberAnnotationExpr; -import com.github.javaparser.ast.expr.SwitchExpr; -import com.github.javaparser.ast.expr.ThisExpr; -import com.github.javaparser.ast.expr.VariableDeclarationExpr; -import com.github.javaparser.ast.stmt.BlockStmt; -import com.github.javaparser.ast.stmt.CatchClause; -import com.github.javaparser.ast.stmt.ExplicitConstructorInvocationStmt; -import com.github.javaparser.ast.stmt.ForEachStmt; -import com.github.javaparser.ast.stmt.ForStmt; -import com.github.javaparser.ast.stmt.IfStmt; -import com.github.javaparser.ast.stmt.ReturnStmt; -import com.github.javaparser.ast.stmt.Statement; -import com.github.javaparser.ast.stmt.SwitchEntry; -import com.github.javaparser.ast.stmt.TryStmt; -import com.github.javaparser.ast.stmt.WhileStmt; -import com.github.javaparser.ast.type.ArrayType; -import com.github.javaparser.ast.type.ClassOrInterfaceType; -import com.github.javaparser.ast.type.ReferenceType; -import com.github.javaparser.ast.type.Type; -import com.github.javaparser.ast.type.TypeParameter; -import com.github.javaparser.ast.type.WildcardType; -import com.github.javaparser.ast.visitor.ModifierVisitor; -import com.github.javaparser.ast.visitor.Visitable; -import com.github.javaparser.resolution.UnsolvedSymbolException; -import com.github.javaparser.resolution.declarations.ResolvedAnnotationDeclaration; -import com.github.javaparser.resolution.declarations.ResolvedMethodDeclaration; -import com.github.javaparser.resolution.declarations.ResolvedReferenceTypeDeclaration; -import com.github.javaparser.resolution.declarations.ResolvedTypeParameterDeclaration; -import com.github.javaparser.resolution.declarations.ResolvedValueDeclaration; -import com.github.javaparser.resolution.types.ResolvedLambdaConstraintType; -import com.github.javaparser.resolution.types.ResolvedReferenceType; -import com.github.javaparser.resolution.types.ResolvedType; -import com.github.javaparser.resolution.types.ResolvedTypeVariable; -import com.github.javaparser.symbolsolver.reflectionmodel.ReflectionAnnotationDeclaration; -import com.github.javaparser.symbolsolver.resolution.typesolvers.JarTypeSolver; -import com.github.javaparser.utils.Pair; -import com.google.common.base.Ascii; -import com.google.common.base.Splitter; -import java.io.BufferedWriter; -import java.io.FileWriter; -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.ArrayDeque; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Iterator; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.Optional; -import java.util.Set; -import java.util.stream.Collectors; -import org.checkerframework.checker.nullness.qual.Nullable; -import org.checkerframework.checker.signature.qual.ClassGetSimpleName; -import org.checkerframework.checker.signature.qual.DotSeparatedIdentifiers; -import org.checkerframework.checker.signature.qual.FullyQualifiedName; -import org.checkerframework.specimin.modularity.ModularityModel; - -/** - * The visitor for the preliminary phase of Specimin. This visitor goes through the input files, - * notices all the methods, fields, and types belonging to classes not in the source codes, and - * creates synthetic versions of those symbols. This preliminary step helps to prevent - * UnsolvedSymbolException errors for the next phases. - * - *

Note: To comprehend this visitor quickly, it is recommended to start by reading all the visit - * methods. - */ -public class UnsolvedSymbolVisitor extends SpeciminStateVisitor { - - /** - * Flag for whether or not to print debugging output. Should always be false except when you are - * actively debugging. - */ - private static final boolean DEBUG = false; - - /** - * This map associates class names with their respective superclasses. The keys in this map - * represent the classes of the currently visited file. Due to the potential presence of inner - * classes, there may be multiple pairs of class and superclass entries in this map. This map can - * also be empty if there are no superclasses other than java.lang.Object involved in the - * currently visited file. - */ - private final Map classAndItsParent = new HashMap<>(); - - /** The package of this class */ - private String currentPackage = ""; - - /** The symbol table to keep track of local variables in the current input file */ - private final ArrayDeque> localVariables = new ArrayDeque>(); - - /** The symbol table for type variables. A type variable is mapped to the list of its bounds. */ - private final ArrayDeque>> typeVariables = - new ArrayDeque<>(); - - /** - * This map will map the name of variables in the current class and its corresponding declaration - */ - private final Map variablesAndDeclaration = new HashMap<>(); - - /** - * Based on the method declarations in the current class, this map will map the name of the - * methods with their corresponding return types - */ - private final Map methodAndReturnType = new HashMap<>(); - - /** List of classes not in the source codes */ - private final Set missingClass = new HashSet<>(); - - /** The same as the root being used in SpeciminRunner */ - private final String rootDirectory; - - /** - * This instance maps the name of the return type of a synthetic method with the synthetic class - * of that method - */ - private final Map syntheticMethodReturnTypeAndClass = - new HashMap<>(); - - /** - * This instance maps the name of a synthetic type with the class where there is a field declared - * with that type - */ - private final Map syntheticTypeAndClass = new HashMap<>(); - - /** - * This is to check if the current synthetic files are enough to prevent UnsolvedSymbolException - * or we still need more. - */ - private boolean gotException; - - /** - * The list of classes that have been created. We use this list to delete all the temporary - * synthetic classes when Specimin finishes its run - */ - private final Set createdClass = new HashSet<>(); - - /** - * List of fully-qualified names of classes that are directly imported (i.e., without the use of a - * wildcard import statement.) - */ - private List importStatement = new ArrayList<>(); - - /** The packages that were imported via wildcard ("*") imports. */ - private final List wildcardImports = new ArrayList<>(1); - - /** This map the classes in the compilation unit with the related package */ - private final Map classAndPackageMap = new HashMap<>(); - - /** This set has fully-qualified class names that come from jar files input */ - private final Set<@FullyQualifiedName String> classesFromJar = new HashSet<>(); - - /** - * A mapping of field name to the name of the class currently being visited and its inner classes - */ - private Map fieldNameToClassNameMap = new HashMap<>(); - - /** - * Mapping of statically imported members where keys are the imported members and values are their - * corresponding classes. - */ - private final Map staticImportedMembersMap = new HashMap<>(); - - /** New files that should be added to the list of target files for the next iteration. */ - private final Set addedTargetFiles = new HashSet<>(); - - /** Stores the sets of method declarations in the currently visiting classes. */ - private final ArrayDeque> declaredMethod = new ArrayDeque<>(); - - /** Maps the name of a class to the list of unsolved interface that it implements. */ - private final Map<@ClassGetSimpleName String, List<@ClassGetSimpleName String>> - classToItsUnsolvedInterface = new HashMap<>(); - - /** - * Fields and methods that could be called inside the target methods. We call them potential-used - * because the usage check is simply based on the simple names of those members. - */ - private final Set potentialUsedMembers = new HashSet<>(); - - /** - * Maps a method reference to all the synthetic method definitions and parameters created from its - * usages. The keys are the method references themselves (i.e. Baz::test), the inner key is the - * unsolved method definition, and the inner value is a Set of the parameter indices where the - * given method reference is used. - */ - private final Map>> methodRefUsageToSyntheticMethodDef = - new HashMap<>(); - - /** - * Check whether the visitor is inside the declaration of a member that could be used by the - * target methods. Symbols inside the declarations of potentially-used members will be solved if - * they have one of the following types: ClassOrInterfaceType, Parameters, and VariableDeclarator. - */ - private boolean insidePotentialUsedMember = false; - - /** - * Indicating whether the visitor is currently visiting the parameter part of a catch block (i.e., - * the "(...)" segment within a catch(...){...} clause). - */ - private boolean isInsideCatchBlockParameter = false; - - /** - * Create a new UnsolvedSymbolVisitor instance - * - * @param rootDirectory the root directory of the input files - * @param existingClassesToFilePath The fully-qualified name of each Java class in the original - * codebase mapped to the corresponding Java file. - * @param targetMethodsSignatures the list of signatures of target methods as specified by the - * user. - * @param targetFieldsSignature the list of signatures of target fields as specified by the user. - * @param model the modularity model selected by the user - */ - public UnsolvedSymbolVisitor( - String rootDirectory, - Map existingClassesToFilePath, - Set targetMethodsSignatures, - Set targetFieldsSignature, - ModularityModel model) { - super( - targetMethodsSignatures, - targetFieldsSignature, - new HashSet<>(), - new HashSet<>(), - model, - existingClassesToFilePath); - this.rootDirectory = rootDirectory; - this.gotException = true; - } - - /** - * Set importStatement equals to the list of import statements from the current compilation unit. - * Also update the classAndPackageMap based on this new list. - * - * @param listOfImports NodeList of import statements from the compilation unit - */ - public void setImportStatement(NodeList listOfImports) { - List currentImportList = new ArrayList<>(); - for (ImportDeclaration importStatement : listOfImports) { - String importAsString = importStatement.getNameAsString(); - currentImportList.add(importAsString); - } - this.importStatement = currentImportList; - this.setclassAndPackageMap(); - } - - /** - * This method sets the value of classesFromJar based on the known class of jar type solvers - * - * @param jarPaths a list of path of jar files - */ - public void setClassesFromJar(List jarPaths) { - for (String path : jarPaths) { - try { - classesFromJar.addAll(new JarTypeSolver(path).getKnownClasses()); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - } - - /** - * This method sets the classAndPackageMap. This method is called in the method - * setImportStatement, as classAndPackageMap and importStatements should always be in sync. - */ - private void setclassAndPackageMap() { - for (String importStatement : this.importStatement) { - List importParts = Splitter.on('.').splitToList(importStatement); - if (!importParts.isEmpty()) { - String className = importParts.get(importParts.size() - 1); - String packageName = importStatement.replace("." + className, ""); - if (!"*".equals(className)) { - this.classAndPackageMap.put(className, packageName); - } - } - } - } - - /** - * This method sets up the value of fieldsAndItsClass by using the result obtained from - * FieldDeclarationsVisitor - * - * @param fieldNameToClassNameMap the value of fieldsAndItsClass from FieldDeclarationsVisitor - */ - public void setFieldNameToClassNameMap( - Map fieldNameToClassNameMap) { - this.fieldNameToClassNameMap = fieldNameToClassNameMap; - } - - /** - * Get the collection of superclasses. Due to the potential presence of inner classes, this method - * returns a collection, as there can be multiple superclasses involved in a single file. - * - * @return the collection of superclasses - */ - public Collection<@ClassGetSimpleName String> getSuperClass() { - return classAndItsParent.values(); - } - - /** - * Get the names of members that could be used by the target methods. - * - * @return a copy of potentialUsedMembers. - */ - public Set getPotentialUsedMembers() { - Set copyOfPotentialUsedMembers = new HashSet<>(); - copyOfPotentialUsedMembers.addAll(potentialUsedMembers); - return copyOfPotentialUsedMembers; - } - - /** - * Return the set of synthetic classes created by the UnsolvedSymbolVisitor in the form of a set - * of strings. - * - * @return the set of created synthetic classes represented as a set of strings. - */ - public Set getSyntheticClassesAsAStringSet() { - Set syntheticClassesAsString = new HashSet<>(); - for (UnsolvedClassOrInterface syntheticClass : missingClass) { - syntheticClassesAsString.add(syntheticClass.toString()); - } - return syntheticClassesAsString; - } - - /** - * Get the value of gotException - * - * @return gotException the value of gotException - */ - public boolean gettingException() { - return gotException; - } - - /** - * Get the classes that have been created by the current iteration of the visitor. - * - * @return createdClass the Set of Path of classes that have beenc created - */ - public Set getCreatedClass() { - return createdClass; - } - - /** - * Set gotException to false. This method is to be used at the beginning of each iteration of the - * visitor. - */ - public void setExceptionToFalse() { - gotException = false; - } - - /** - * Get the set of target files that should be added for the next iteration. - * - * @return a copy of addedTargetFiles. - */ - public Set getAddedTargetFiles() { - Set copyOfTargetFiles = new HashSet<>(); - copyOfTargetFiles.addAll(addedTargetFiles); - return copyOfTargetFiles; - } - - @Override - public Node visit(ImportDeclaration decl, Void arg) { - /* - * This method visits an import declaration in the currently visiting CompilationUnit and update the content of wildCardImports and staticImportedMembersMap accordingly. - */ - - if (decl.isAsterisk()) { - wildcardImports.add(decl.getNameAsString()); - } - if (decl.isStatic()) { - String name = decl.getNameAsString(); - @SuppressWarnings( - "signature") // since this is from an import statement, this is a fully qualified class - // name - @FullyQualifiedName String className = name.substring(0, name.lastIndexOf(".")); - String elementName = name.replace(className + ".", ""); - staticImportedMembersMap.put(elementName, className); - } - return super.visit(decl, arg); - } - - @Override - public Visitable visit(PackageDeclaration node, Void arg) { - this.currentPackage = node.getNameAsString(); - return super.visit(node, arg); - } - - /** - * Maintains the data structures of this class (like the {@link #className}, {@link - * #currentClassQualifiedName}, {@link #addedTargetFiles}, etc.) based on a class, interface, or - * enum declaration. Call this method before calling super.visit(). - * - * @param decl the class, interface, or enum declaration - */ - @Override - protected void maintainDataStructuresPreSuper(TypeDeclaration decl) { - super.maintainDataStructuresPreSuper(decl); - if (decl.isEnumDeclaration()) { - // Enums cannot extend other classes (they always extend Enum) and cannot have type - // parameters, so it's not necessary to do any maintenance on the data structures that - // track superclasses or type parameters in the enum case (only implemented interfaces). - NodeList implementedTypes = - decl.asEnumDeclaration().getImplementedTypes(); - updateForExtendedAndImplementedTypes(implementedTypes, implementedTypes, false); - } else if (decl.isClassOrInterfaceDeclaration()) { - ClassOrInterfaceDeclaration asClassOrInterface = decl.asClassOrInterfaceDeclaration(); - - // Maintenance of type parameters - addTypeVariableScope(asClassOrInterface.getTypeParameters()); - - // Maintenance of superclasses and implemented/extended classes. - if (asClassOrInterface.getExtendedTypes().isNonEmpty()) { - // note that since Specimin does not have access to the classpaths of the project, all the - // unsolved methods related to inheritance will be placed in the parent class, even if there - // is a grandparent class and so forth. - SimpleName superClassSimpleName = asClassOrInterface.getExtendedTypes().get(0).getName(); - classAndItsParent.put(className, superClassSimpleName.asString()); - } - NodeList implementedTypes = asClassOrInterface.getImplementedTypes(); - // Not sure why getExtendedTypes return a list, since a class can only extends at most one - // class in Java. - NodeList extendedAndImplementedTypes = - asClassOrInterface.getExtendedTypes(); - extendedAndImplementedTypes.addAll(implementedTypes); - - // Also include the bounds of the class' type parameters. - for (TypeParameter t : asClassOrInterface.getTypeParameters()) { - NodeList bounds = t.getTypeBound(); - for (int i = 0; i < bounds.size(); i++) { - // In Java, only the first bound may be a class; subsequent bounds _must_ be - // interfaces. Therefore, we have to add bounds later than the first to both - // the extended and the implemented types. - extendedAndImplementedTypes.add(bounds.get(i)); - if (i > 0) { - implementedTypes.add(bounds.get(i)); - } - } - } - - updateForExtendedAndImplementedTypes( - extendedAndImplementedTypes, implementedTypes, asClassOrInterface.isInterface()); - } else { - throw new RuntimeException( - "unexpected type of declaration; expected a class, interface, or enum: " + decl); - } - declaredMethod.addFirst(new HashSet<>(decl.getMethods())); - } - - /** - * Maintains the data structures of this class (like the {@link #className}, {@link - * #currentClassQualifiedName}, {@link #addedTargetFiles}, etc.) based on a class, interface, or - * enum declaration. Call this method after calling super.visit(). - * - * @param decl the class, interface, or enum declaration - */ - @Override - protected void maintainDataStructuresPostSuper(TypeDeclaration decl) { - if (decl.isClassOrInterfaceDeclaration()) { - // Enums don't have type variables, so no scope for them is created - // when entering an enum. - typeVariables.removeFirst(); - } - - declaredMethod.removeFirst(); - super.maintainDataStructuresPostSuper(decl); - } - - /** - * Updates the list of classes/interfaces to keep based on the extends/implements clauses of a - * class, interface, or enum. Does not side effect its arguments, so it's safe to pass the same - * value for the first two arguments (e.g., if this is an enum, which cannot extend anything). - * - * @param extendedAndImplementedTypes the list of extended and implemented classes/interfaces - * @param implementedTypes the list of implemented interfaces - * @param isAnInterface is the node whose extends/implements clauses are being considered an - * interface - */ - private void updateForExtendedAndImplementedTypes( - NodeList extendedAndImplementedTypes, - NodeList implementedTypes, - boolean isAnInterface) { - for (ClassOrInterfaceType implementedOrExtended : extendedAndImplementedTypes) { - String qualifiedName = getQualifiedNameForClassOrInterfaceType(implementedOrExtended); - if (classfileIsInOriginalCodebase(qualifiedName)) { - // add the source codes of the interface or the super class to the list of target files so - // that UnsolvedSymbolVisitor can solve symbols for that class if needed. - String filePath = qualifiedNameToFilePath(qualifiedName); - if (!addedTargetFiles.contains(filePath)) { - // strictly speaking, there is no exception here. But we set gotException to true so that - // UnsolvedSymbolVisitor will run at least one more iteration to visit the newly added - // file. - gotException(); - } - addedTargetFiles.add(filePath); - } else { - try { - implementedOrExtended.resolve(); - continue; - } - // IllegalArgumentException is thrown when implementedOrExtended has a generic type. - catch (UnsolvedSymbolException | IllegalArgumentException e) { - // this extended/implemented type is an interface if it is in the declaration of an - // interface, or if it is used with the "implements" keyword. - boolean typeIsAnInterface = - isAnInterface || implementedTypes.contains(implementedOrExtended); - if (typeIsAnInterface) { - solveSymbolsForClassOrInterfaceType(implementedOrExtended, true); - @SuppressWarnings( - "signature") // an empty array list is not a list of @ClassGetSimpleName, but since - // we will add typeName to that list right after the initialization, - // this code is correct. - List<@ClassGetSimpleName String> interfaceName = - classToItsUnsolvedInterface.computeIfAbsent(className, k -> new ArrayList<>()); - interfaceName.add(implementedOrExtended.getName().asString()); - } else { - solveSymbolsForClassOrInterfaceType(implementedOrExtended, false); - } - } - } - } - } - - @Override - public Visitable visit(ExplicitConstructorInvocationStmt node, Void arg) { - /* - * This methods create synthetic classes for unsolved explicit constructor invocation, such as super(). We only solve the invocation after all of its arguments have been solved. - */ - if (node.isThis()) { - return super.visit(node, arg); - } - if (!insideTargetMember) { - return super.visit(node, arg); - } - if (!canSolveArguments(node.getArguments())) { - // wait for the next run of UnsolvedSymbolVisitor - return super.visit(node, arg); - } - - try { - // check if the symbol is solvable. If it is, then there's no need to create a synthetic file. - node.resolve().getQualifiedSignature(); - return super.visit(node, arg); - } catch (Exception e) { - NodeList arguments = node.getArguments(); - String pkgName = getPackageFromClassName(getParentClass(className)); - List argList = getArgumentTypesImpl(arguments, pkgName); - UnsolvedMethod constructorMethod = new UnsolvedMethod(getParentClass(className), "", argList); - // if the parent class can not be found in the import statements, Specimin assumes it is in - // the same package as the child class. - UnsolvedClassOrInterface superClass = - new UnsolvedClassOrInterface(getParentClass(className), pkgName); - superClass.addMethod(constructorMethod); - - updateUnsolvedMethodsWithMethodReferences(node, constructorMethod); - updateMissingClass(superClass); - return super.visit(node, arg); - } - } - - @Override - public Visitable visit(ForStmt node, Void p) { - HashSet currentLocalVariables = new HashSet<>(); - localVariables.addFirst(currentLocalVariables); - Visitable result = super.visit(node, p); - localVariables.removeFirst(); - return result; - } - - @Override - @SuppressWarnings("nullness:override") - public @Nullable Visitable visit(IfStmt n, Void arg) { - /* - * This method is a copy from the original visit(IfStmt, Void) from JavaParser. We add additional codes here to update the set of local variables. - */ - HashSet localVarInCon = new HashSet<>(); - localVariables.addFirst(localVarInCon); - Expression condition = (Expression) n.getCondition().accept(this, arg); - localVariables.removeFirst(); - localVarInCon = new HashSet<>(); - localVariables.addFirst(localVarInCon); - Statement elseStmt = n.getElseStmt().map(s -> (Statement) s.accept(this, arg)).orElse(null); - localVariables.removeFirst(); - localVarInCon = new HashSet<>(); - localVariables.addFirst(localVarInCon); - Statement thenStmt = (Statement) n.getThenStmt().accept(this, arg); - localVariables.removeFirst(); - if (condition == null || thenStmt == null) { - return null; - } - n.setCondition(condition); - n.setElseStmt(elseStmt); - n.setThenStmt(thenStmt); - return n; - } - - @Override - public Visitable visit(WhileStmt node, Void p) { - HashSet currentLocalVariables = new HashSet<>(); - localVariables.addFirst(currentLocalVariables); - Visitable result = super.visit(node, p); - localVariables.removeFirst(); - return result; - } - - @Override - public Visitable visit(ForEachStmt node, Void p) { - HashSet currentLocalVariables = new HashSet<>(); - localVariables.addFirst(currentLocalVariables); - String loopVarName = node.getVariableDeclarator().getNameAsString(); - currentLocalVariables.add(loopVarName); - Visitable result = super.visit(node, p); - localVariables.removeFirst(); - return result; - } - - @Override - public Visitable visit(SwitchExpr node, Void p) { - HashSet currentLocalVariables = new HashSet<>(); - localVariables.addFirst(currentLocalVariables); - Visitable result = super.visit(node, p); - localVariables.removeFirst(); - return result; - } - - @Override - public Visitable visit(SwitchEntry node, Void p) { - HashSet currentLocalVariables = new HashSet<>(); - localVariables.addFirst(currentLocalVariables); - Visitable result = super.visit(node, p); - localVariables.removeFirst(); - return result; - } - - @Override - public Visitable visit(TryStmt node, Void p) { - HashSet currentLocalVariables = new HashSet<>(); - localVariables.addFirst(currentLocalVariables); - List resources = node.getResources(); - if (!resources.isEmpty()) { - handleSyntheticResources(resources); - } - Visitable result = super.visit(node, p); - localVariables.removeFirst(); - return result; - } - - /** - * Ensures that every type used by a try-with-resources statement extends java.lang.AutoCloseable. - * - * @param resources a list of resource expressions - */ - private void handleSyntheticResources(List resources) { - // Resource expressions can be: - // * names - // * field accesses - // * a new local variable declaration - // In the former two cases, we have to wait to handle the synthetic resources - // until the expression can be solved. For the latter, we have to wait until the - // declared type is solvable. - for (Expression resource : resources) { - if (resource.isVariableDeclarationExpr()) { - VariableDeclarationExpr asVar = resource.asVariableDeclarationExpr(); - String fqn; - try { - fqn = asVar.calculateResolvedType().describe(); - } catch (UnsolvedSymbolException e) { - gotException(); - continue; - } - makeClassAutoCloseable(fqn); - } else if (resource.isNameExpr()) { - NameExpr asName = resource.asNameExpr(); - String fqn; - try { - fqn = asName.resolve().getType().describe(); - } catch (UnsolvedSymbolException e) { - gotException(); - continue; - } - makeClassAutoCloseable(fqn); - } else if (resource.isFieldAccessExpr()) { - FieldAccessExpr asField = resource.asFieldAccessExpr(); - String fqn; - try { - fqn = asField.resolve().getType().describe(); - } catch (UnsolvedSymbolException e) { - gotException(); - continue; - } - makeClassAutoCloseable(fqn); - } else { - throw new RuntimeException( - "unexpected type of node in a try-with-resources expression: " - + resource.getClass() - + "\nresouce was " - + resource); - } - } - } - - /** - * Makes the synthetic class with the given name implement AutoCloseable, if such a synthetic - * class exists. If not, silently does nothing, since that should only happen when encounting a - * non-synthetic class, which must already implement AutoCloseable if it is used in a - * try-with-resources that compiles (i.e., this method relies on the assumption that the input - * compiles). - * - * @param fqn a fully-qualified name - */ - private void makeClassAutoCloseable(String fqn) { - for (UnsolvedClassOrInterface sytheticClass : missingClass) { - if (sytheticClass.getQualifiedClassName().equals(fqn)) { - sytheticClass.implement("java.lang.AutoCloseable"); - sytheticClass.addMethod(UnsolvedMethod.CLOSE); - } - } - } - - @Override - @SuppressWarnings("nullness") - // This method returns a nullable result, and "comment" can be null for the phrase - // node.setComment(comment). - // These are the codes from JavaParser, so we optimistically assume that these lines are safe. - public Visitable visit(CatchClause node, Void arg) { - /* - * This method is a copy from the visit(CatchClause, Void) method of JavaParser. We extend it to update the set of local variables and the flag isInsideCatchBlockParameter - */ - HashSet currentLocalVariables = new HashSet<>(); - currentLocalVariables.add(node.getParameter().getNameAsString()); - localVariables.addFirst(currentLocalVariables); - BlockStmt body = (BlockStmt) node.getBody().accept(this, arg); - // There can not be a parameter list inside a parameter list, hence we don't need a temporary - // local variable like in the case of insideTargetMethod. - isInsideCatchBlockParameter = true; - Parameter parameter = (Parameter) node.getParameter().accept(this, arg); - isInsideCatchBlockParameter = false; - Comment comment = node.getComment().map(s -> (Comment) s.accept(this, arg)).orElse(null); - if (body == null || parameter == null) { - localVariables.removeFirst(); - return null; - } - node.setBody(body); - node.setParameter(parameter); - node.setComment(comment); - localVariables.removeFirst(); - return node; - } - - @Override - public Visitable visit(BlockStmt node, Void p) { - HashSet currentLocalVariables = new HashSet<>(); - localVariables.addFirst(currentLocalVariables); - Visitable result = super.visit(node, p); - localVariables.removeFirst(); - return result; - } - - @Override - public Visitable visit(InstanceOfExpr node, Void p) { - // If we have x : X and x instanceof Y, then X must be a supertype - // of Y if X != Y. The JLS says (15.20.2): "If a cast of the RelationalExpression to the - // ReferenceType would be rejected as a compile-time error, then the instanceof relational - // expression likewise produces a compile-time error. In such a situation, the result of the - // instanceof expression could never be true." - // - // This visit method uses this fact to add extends clauses to classes created by - // UnsolvedSymbolVisitor. - ReferenceType referenceType; - if (node.getPattern().isPresent()) { - PatternExpr patternExpr = node.getPattern().get(); - referenceType = patternExpr.getType(); - } else { - referenceType = node.getType(); - } - Expression relationalExpr = node.getExpression(); - String relationalExprFQN, referenceTypeFQN; - try { - referenceTypeFQN = referenceType.resolve().describe(); - relationalExprFQN = relationalExpr.calculateResolvedType().describe(); - } catch (UnsolvedSymbolException e) { - // Try again next time. - this.gotException(); - return super.visit(node, p); - } - - if (referenceTypeFQN.equals(relationalExprFQN)) { - // A type can't extend itself. - return super.visit(node, p); - } - - for (UnsolvedClassOrInterface syntheticClass : missingClass) { - if (syntheticClass.getQualifiedClassName().equals(referenceTypeFQN)) { - // TODO: check for double extends? - syntheticClass.extend(relationalExprFQN); - break; - } - } - - return super.visit(node, p); - } - - @Override - public Visitable visit(LambdaExpr node, Void p) { - boolean noLocalScope = localVariables.isEmpty(); - if (noLocalScope) { - localVariables.addFirst(new HashSet<>()); - } - // add the parameters to the local variable map - // Note that lambdas DO NOT CREATE A NEW SCOPE - // (why? ask whoever designed the feature...) - for (Parameter lambdaParam : node.getParameters()) { - localVariables.getFirst().add(lambdaParam.getNameAsString()); - } - - Visitable result = super.visit(node, p); - - // then remove them - if (noLocalScope) { - localVariables.removeFirst(); - } else { - for (Parameter lambdaParam : node.getParameters()) { - localVariables.getFirst().remove(lambdaParam.getNameAsString()); - } - } - return result; - } - - @Override - public Visitable visit(VariableDeclarator decl, Void p) { - boolean oldInsidePotentialUsedMember = insidePotentialUsedMember; - // This part is to update the symbol table. - boolean isAField = - decl.getParentNode().isPresent() - && (decl.getParentNode().get() instanceof FieldDeclaration); - if (!isAField) { - Set currentListOfLocals = localVariables.peek(); - if (currentListOfLocals == null) { - throw new RuntimeException("tried to add a variable without a scope available: " + decl); - } - currentListOfLocals.add(decl.getNameAsString()); - } - if (potentialUsedMembers.contains(decl.getName().asString())) { - insidePotentialUsedMember = true; - } - if (!insideTargetMember && !insidePotentialUsedMember) { - return super.visit(decl, p); - } - - // This part is to create synthetic class for the type of decl if needed. - Type declType = decl.getType(); - if (declType.isVarType()) { - // nothing to do here. A var type could never be solved. - Visitable result = super.visit(decl, p); - insidePotentialUsedMember = oldInsidePotentialUsedMember; - return result; - } - try { - declType.resolve(); - } catch (UnsolvedSymbolException | UnsupportedOperationException e) { - String typeAsString = declType.asString(); - List elements = Splitter.onPattern("\\.").splitToList(typeAsString); - // There could be three cases here: a type variable, a fully-qualified class name, or a simple - // class name. - // This is the fully-qualified case. - if (elements.size() > 1) { - int typeParamIndex = typeAsString.indexOf('<'); - int typeParamCount = -1; - if (typeParamIndex != -1) { - ClassOrInterfaceType asType = StaticJavaParser.parseClassOrInterfaceType(typeAsString); - typeParamCount = asType.getTypeArguments().get().size(); - typeAsString = typeAsString.substring(0, typeParamIndex); - } - - @SuppressWarnings( - "signature") // since this type is in a fully-qualified form, or we make it - // fully-qualified - @FullyQualifiedName String qualifiedTypeName = - typeAsString.contains(".") - ? typeAsString - : getPackageFromClassName(typeAsString) + "." + typeAsString; - UnsolvedClassOrInterface unsolved = - getSimpleSyntheticClassFromFullyQualifiedName(qualifiedTypeName); - if (typeParamCount != -1) { - unsolved.setNumberOfTypeVariables(typeParamCount); - } - updateMissingClass(unsolved); - } else if (isTypeVar(typeAsString)) { - // Nothing to do in this case, but we need to skip creating an unsolved class. - } - // Handles the case where the type is a simple class name. Two sub-cases are considered: 1. - // The class is included among the import statements. 2. The class is not included in the - // import statements but is in the same directory as the input class. The first sub-case is - // addressed by the visit method for ImportDeclaration. - else if (!classAndPackageMap.containsKey(typeAsString)) { - @SuppressWarnings("signature") // since this is the simple name case - @ClassGetSimpleName String className = typeAsString; - String packageName = getPackageFromClassName(className); - UnsolvedClassOrInterface newClass = new UnsolvedClassOrInterface(className, packageName); - updateMissingClass(newClass); - } - } - Visitable result = super.visit(decl, p); - insidePotentialUsedMember = oldInsidePotentialUsedMember; - return result; - } - - @Override - public Visitable visit(NameExpr node, Void arg) { - Optional parent = node.getParentNode(); - - boolean insideAnnotation = parent.isPresent() && (parent.get() instanceof AnnotationExpr); - - if (!insideTargetMember && !insideAnnotation) { - return super.visit(node, arg); - } - String name = node.getNameAsString(); - if (fieldNameToClassNameMap.containsKey(name)) { - potentialUsedMembers.add(name); - if (!canBeSolved(node)) { - gotException(); - } else { - // check if all the type parameters are resolved. - ResolvedType nameExprType = node.resolve().getType(); - if (nameExprType.isReferenceType()) { - ResolvedReferenceType nameExprReferenceType = nameExprType.asReferenceType(); - nameExprReferenceType.getAllAncestors(); - if (!hasResolvedTypeParameters(nameExprReferenceType)) { - gotException(); - } - } - } - return super.visit(node, arg); - } - // this condition checks if this NameExpr is a statically imported field - else if (staticImportedMembersMap.containsKey(name)) { - try { - node.resolve(); - } catch (UnsolvedSymbolException e) { - @FullyQualifiedName String className = staticImportedMembersMap.get(name); - String fullyQualifiedFieldSignature = className + "." + name; - updateClassSetWithQualifiedFieldSignature(fullyQualifiedFieldSignature, true, true); - return super.visit(node, arg); - } - } - // This method explicitly handles NameExpr instances that represent fields of classes but are - // not explicitly shown in the code. For example, if "number" is a field of a class, then - // "return number;" is an expression that this method will address. If the NameExpr instance is - // not a field of any class, or if it is a field of a class but is explicitly referenced, such - // as "Math.number," we handle it in other visit methods. - if (!canBeSolved(node)) { - Optional parentNode = node.getParentNode(); - // we take care of MethodCallExpr and FieldAccessExpr cases in other visit methods - if (parentNode.isEmpty() - || !(parentNode.get() instanceof MethodCallExpr - || parentNode.get() instanceof FieldAccessExpr)) { - if (!isALocalVar(name)) { - updateSyntheticClassForSuperCall(node); - } - } - } - return super.visit(node, arg); - } - - @Override - public Visitable visit(FieldDeclaration node, Void arg) { - for (VariableDeclarator var : node.getVariables()) { - String variableName = var.getNameAsString(); - String variableType = node.getElementType().asString(); - Optional potentialValue = var.getInitializer(); - String variableDeclaration = variableType + " " + variableName; - if (potentialValue.isPresent()) { - String variableValue = potentialValue.get().toString(); - variableDeclaration += " = " + variableValue; - } else { - variableDeclaration = - this.setInitialValueForVariableDeclaration(variableType, variableDeclaration); - } - variablesAndDeclaration.put(variableName, variableDeclaration); - } - return super.visit(node, arg); - } - - @Override - public Visitable visit(ConstructorDeclaration node, Void arg) { - boolean oldInsidePotentialUsedMember = insidePotentialUsedMember; - if (potentialUsedMembers.contains(node.getNameAsString())) { - insidePotentialUsedMember = true; - } - addTypeVariableScope(node.getTypeParameters()); - if (targetMethods.contains(getSignature(node))) { - // If this constructor is a target method, and the modularity model - // permits reasoning about field assignments in constructors, then - // we need to preserve the types of all of the fields declared in the - // class. - if (modularityModel.preserveAllFieldsIfTargetIsConstructor()) { - // This cast is safe, because a constructor must be contained in a class declaration. - ClassOrInterfaceDeclaration thisClass = - (ClassOrInterfaceDeclaration) JavaParserUtil.getEnclosingClassLike(node); - for (FieldDeclaration field : thisClass.getFields()) { - for (VariableDeclarator variable : field.getVariables()) { - Type type = variable.getType(); - resolveTypeExpr(type); - } - } - } - } - Visitable result = super.visit(node, arg); - typeVariables.removeFirst(); - insidePotentialUsedMember = oldInsidePotentialUsedMember; - return result; - } - - @SuppressWarnings( - "nullness:return") // return type is not used, and we need to avoid calling super.visit() - @Override - public Visitable visit(MethodDeclaration node, Void arg) { - // Duplicative with super, but needed to maintain the if...else... structure below. - String methodQualifiedSignature = - this.currentClassQualifiedName - + "#" - + JavaParserUtil.removeMethodReturnTypeSpacesAndAnnotations(node); - String methodSimpleName = node.getName().asString(); - if (targetMethods.contains(methodQualifiedSignature)) { - Visitable result = processMethodDeclaration(node); - return result; - } else if (potentialUsedMembers.contains(methodSimpleName)) { - boolean oldInsidePotentialUsedMember = insidePotentialUsedMember; - insidePotentialUsedMember = true; - Visitable result = processMethodDeclaration(node); - insidePotentialUsedMember = oldInsidePotentialUsedMember; - return result; - } else if (insideTargetMember) { - return processMethodDeclaration(node); - } else { - // Do not call super.visit(): this method is definitely unused by the targets, and so - // there's no reason to solve its symbols. Furthermore, doing so may lead to data - // structure corruption (e.g., of type variables), since processMethodDeclaration, - // which does data structure management, will not be called. - return null; - } - } - - @Override - public Visitable visit(FieldAccessExpr node, Void p) { - if (!insideTargetMember) { - return super.visit(node, p); - } - potentialUsedMembers.add(node.getNameAsString()); - boolean canBeSolved = canBeSolved(node); - if (isASuperCall(node) && !canBeSolved) { - updateSyntheticClassForSuperCall(node); - } else if (updatedAddedTargetFilesForPotentialEnum(node)) { - return super.visit(node, p); - } else if (canBeSolved) { - return super.visit(node, p); - } else if (isAQualifiedFieldSignature(node.toString())) { - updateClassSetWithQualifiedFieldSignature(node.toString(), true, false); - } else if (unsolvedFieldCalledByASimpleClassName(node)) { - String simpleClassName = node.getScope().toString(); - String fullyQualifiedCall = getPackageFromClassName(simpleClassName) + "." + node; - updateClassSetWithQualifiedFieldSignature(fullyQualifiedCall, true, false); - } else if (canBeSolved(node.getScope())) { - // check if this unsolved field belongs to a synthetic class. - if (!belongsToARealClassFile(node)) { - updateSyntheticClassWithNonStaticFields(node); - } else { - // since we have checked whether node.getScope() can be solved, this call is safe. - addedTargetFiles.add( - qualifiedNameToFilePath( - node.getScope().calculateResolvedType().asReferenceType().getQualifiedName())); - } - } - - try { - node.resolve(); - } catch (UnsolvedSymbolException | UnsupportedOperationException e) { - // for a qualified name field access such as org.sample.MyClass.field, org.sample will also be - // considered FieldAccessExpr. - if (JavaParserUtil.isAClassPath(node.getScope().toString())) { - gotException(); - } - } - return super.visit(node, p); - } - - @Override - public Visitable visit(MethodReferenceExpr node, Void p) { - if (insideTargetMember) { - // TODO: handle all of the possible forms listed in JLS 15.13, not just the simplest - Expression scope = node.getScope(); - if (scope.isTypeExpr()) { - Type scopeAsType = scope.asTypeExpr().getType(); - String scopeAsTypeFQN = scopeAsType.asString(); - if (!JavaParserUtil.isAClassPath(scopeAsTypeFQN) && scopeAsType.isClassOrInterfaceType()) { - scopeAsTypeFQN = - getQualifiedNameForClassOrInterfaceType(scopeAsType.asClassOrInterfaceType()); - } - if (classfileIsInOriginalCodebase(scopeAsTypeFQN)) { - addedTargetFiles.add(qualifiedNameToFilePath(scopeAsTypeFQN)); - } else { - // TODO: create a synthetic class? - } - } - String identifier = node.getIdentifier(); - // can be either the name of a method or "new" - if ("new".equals(identifier)) { - // TODO: figure out how to handle this case - System.err.println("Specimin warning: new in method references is not supported: " + node); - return super.visit(node, p); - } - potentialUsedMembers.add(identifier); - } - return super.visit(node, p); - } - - @Override - public Visitable visit(MethodCallExpr method, Void p) { - /* - * There's a specific order in which we resolve symbols for a method call. - * We ensure that the caller and its parameters are resolved before solving the method itself. - * For instance, in a method call like a.b(c, d, e,...), we solve a, c, d, e,... before resolving b. - */ - if (!insideTargetMember) { - return super.visit(method, p); - } - potentialUsedMembers.add(method.getName().asString()); - if (canBeSolved(method) && isFromAJarFile(method)) { - updateClassesFromJarSourcesForMethodCall(method); - return super.visit(method, p); - } - // we will wait for the next run to solve this method call - if (!canSolveArguments(method.getArguments())) { - return super.visit(method, p); - } - if (isASuperCall(method) && !canBeSolved(method)) { - updateSyntheticClassForSuperCall(method); - return super.visit(method, p); - } - String methodName = method.getNameAsString(); - if (isAnUnsolvedStaticMethodCalledByAQualifiedClassName(method)) { - updateClassSetWithStaticMethodCall(method); - } else if (unsolvedAndCalledByASimpleClassName(method)) { - updateClassSetWithStaticMethodCall(method); - } else if (calledByAnIncompleteClass(method)) { - /* - * Note that the body here assumes that the method is not static. This assumption is safe since we have isAnUnsolvedStaticMethodCalledByAQualifiedClassName(method) and unsolvedAndCalledByASimpleClassName(method) before this condition. - */ - String qualifiedNameOfIncompleteClass = getIncompleteClass(method); - if (classfileIsInOriginalCodebase(qualifiedNameOfIncompleteClass)) { - addedTargetFiles.add(qualifiedNameToFilePath(qualifiedNameOfIncompleteClass)); - } else { - updateUnsolvedClassOrInterfaceWithMethod(method, qualifiedNameOfIncompleteClass, "", false); - } - } else if (staticImportedMembersMap.containsKey(methodName)) { - @FullyQualifiedName String className = staticImportedMembersMap.get(methodName); - String methodFullyQualifiedCall = className + "." + methodName; - String pkgName = className.substring(0, className.lastIndexOf('.')); - // everything inside the (...) will be trimmed - updateClassSetWithQualifiedStaticMethodCall( - methodFullyQualifiedCall + "()", getArgumentTypesFromMethodCall(method, pkgName)); - } else if (haveNoScopeOrCallByThisKeyword(method)) { - // in this case, the method must be declared inside the interface or the superclass that the - // current class extends/implements. - if (!declaredInCurrentClass(method)) { - if (classToItsUnsolvedInterface.containsKey(className)) { - List<@ClassGetSimpleName String> relevantInterfaces = - classToItsUnsolvedInterface.get(className); - // Since these are unsolved interfaces, we have no ideas which one of them contains the - // signature for the current method, thus we will put the signature in the last interface. - String unsolvedInterface = relevantInterfaces.get(relevantInterfaces.size() - 1); - updateUnsolvedClassOrInterfaceWithMethod(method, unsolvedInterface, "", true); - } else if (classAndItsParent.containsKey(className)) { - String parentName = classAndItsParent.get(className); - updateUnsolvedClassOrInterfaceWithMethod(method, parentName, "", false); - } - } - } - - // Though this structure looks a bit silly, it is intentional - // that these 4 calls to getException() produce different stacktraces, - // which is very helpful for debugging infinite loops. - if (!canBeSolved(method)) { - gotException(); - } else if (calledByAnUnsolvedSymbol(method)) { - gotException(); - } else if (calledByAnIncompleteClass(method)) { - gotException(); - } else if (isAnUnsolvedStaticMethodCalledByAQualifiedClassName(method)) { - gotException(); - } - return super.visit(method, p); - } - - @Override - public Visitable visit(EnumConstantDeclaration expr, Void p) { - // this is a bit hacky, but we don't remove any enum constant declarations if they are ever - // used, so it's safer to just preserve anything that they use by pretending that we're inside a - // target method. - boolean oldInsideTargetMember = insideTargetMember; - insideTargetMember = true; - Visitable result = super.visit(expr, p); - insideTargetMember = oldInsideTargetMember; - return result; - } - - @Override - public Visitable visit(ClassOrInterfaceType typeExpr, Void p) { - // Workaround for a JavaParser bug: When a type is referenced using its fully-qualified name, - // like com.example.Dog dog, JavaParser considers its package components (com and com.example) - // as types, too. This issue happens even when the source file of the Dog class is present in - // the codebase. - if (!JavaParserUtil.isCapital(typeExpr.getName().asString())) { - return super.visit(typeExpr, p); - } - // type belonging to a class declaration will be handled by the visit method for - // ClassOrInterfaceDeclaration - if (typeExpr.getParentNode().get() instanceof ClassOrInterfaceDeclaration) { - return super.visit(typeExpr, p); - } - if (!insideTargetMember && !insidePotentialUsedMember) { - return super.visit(typeExpr, p); - } - resolveTypeExpr(typeExpr); - - return super.visit(typeExpr, p); - } - - @Override - public Visitable visit(WildcardType type, Void p) { - if (!insideTargetMember && !insidePotentialUsedMember) { - return super.visit(type, p); - } - resolveTypeExpr(type); - return super.visit(type, p); - } - - @Override - public Visitable visit(ArrayType type, Void p) { - if (!insideTargetMember && !insidePotentialUsedMember) { - return super.visit(type, p); - } - resolveTypeExpr(type); - return super.visit(type, p); - } - - /** - * Shared logic for checking whether type expressions (e.g., ArrayType, ClassOrInterfaceType, - * etc.) are resolvable, and creating synthetic classes if not. - * - * @param type a type expression - */ - private void resolveTypeExpr(Type type) { - if (type.isArrayType()) { - resolveTypeExpr(type.asArrayType().getComponentType()); - return; - } - - if (type.isWildcardType()) { - Optional extended = type.asWildcardType().getExtendedType(); - if (extended.isPresent()) { - resolveTypeExpr(extended.get()); - } - Optional sup = type.asWildcardType().getSuperType(); - if (sup.isPresent()) { - resolveTypeExpr(sup.get()); - } - return; - } - - if (!type.isClassOrInterfaceType()) { - return; - } - ClassOrInterfaceType typeExpr = type.asClassOrInterfaceType(); - - if (isTypeVar(typeExpr.getName().asString())) { - updateSyntheticClassesForTypeVar(typeExpr); - return; - } - if (updateTargetFilesListForExistingClassWithInheritance(typeExpr)) { - return; - } - try { - // resolve() checks whether this type is resolved. getAllAncestor() checks whether this type - // extends or implements a resolved class/interface. - JavaParserUtil.classOrInterfaceTypeToResolvedReferenceType(typeExpr).getAllAncestors(); - return; - } - /* - * 1. If the class file is in the codebase but extends/implements a class/interface not in the codebase, we got UnsolvedSymbolException. - * 2. If the class file is not in the codebase yet, we also got UnsolvedSymbolException. - * 3. If the class file is not in the codebase and used by an anonymous class, we got UnsupportedOperationException. - * 4. If the class file is in the codebase but the type variables are missing, we got IllegalArgumentException. - */ - catch (UnsolvedSymbolException | UnsupportedOperationException | IllegalArgumentException e) { - // this is for case 1. By adding the class file to the list of target files, - // UnsolvedSymbolVisitor will take care of the unsolved extension in its next iteration. - String qualifiedName = - getPackageFromClassName(typeExpr.getNameAsString()) + "." + typeExpr.getNameAsString(); - if (classfileIsInOriginalCodebase(qualifiedName)) { - addedTargetFiles.add(qualifiedNameToFilePath(qualifiedName)); - gotException(); - return; - } - - // below is for other three cases. - - // This method only updates type variables for unsolved classes. Other problems causing a - // class to be unsolved will be fixed by other methods. - String typeRawName = typeExpr.getElementType().asString(); - if (typeExpr.isClassOrInterfaceType() - && typeExpr.asClassOrInterfaceType().getTypeArguments().isPresent()) { - // remove type arguments - typeRawName = typeRawName.substring(0, typeRawName.indexOf("<")); - } - - if (isTypeVar(typeRawName)) { - // If the type name itself is an in-scope type variable, just return without attempting - // to create a missing class. - return; - } - solveSymbolsForClassOrInterfaceType(typeExpr, false); - gotException(); - } - } - - @Override - public Visitable visit(ObjectCreationExpr newExpr, Void p) { - String oldClassName = className; - if (!insideTargetMember) { - if (newExpr.getAnonymousClassBody().isPresent()) { - // Need to do data structure maintenance - className = newExpr.getType().getName().asString(); - } - Visitable result = super.visit(newExpr, p); - className = oldClassName; - return result; - } - potentialUsedMembers.add(newExpr.getTypeAsString()); - // Cannot be newExpr.getTypeAsString(), because that will include type variables, - // which is undesirable. - String type = newExpr.getType().getNameAsString(); - if (canBeSolved(newExpr)) { - if (isFromAJarFile(newExpr)) { - updateClassesFromJarSourcesForObjectCreation(newExpr); - } - if (newExpr.getAnonymousClassBody().isPresent()) { - // Need to do data structure maintenance - className = newExpr.getType().getName().asString(); - } - Visitable result = super.visit(newExpr, p); - className = oldClassName; - return result; - } - gotException(); - /* - * For an unresolved object creation, the arguments are resolved first before the expression itself is resolved. - */ - try { - List argumentsCreation = - getArgumentTypesFromObjectCreation(newExpr, getPackageFromClassName(type)); - UnsolvedMethod creationMethod = new UnsolvedMethod("", type, argumentsCreation); - updateUnsolvedMethodsWithMethodReferences(newExpr, creationMethod); - - updateUnsolvedClassWithClassName(type, false, false, creationMethod); - } catch (Exception q) { - // The exception originates from the call to getArgumentTypesFromObjectCreation within the try - // block, indicating unresolved parameters in this object creation. - gotException(); - } - if (newExpr.getAnonymousClassBody().isPresent()) { - // Need to do data structure maintenance - className = newExpr.getType().getName().asString(); - } - Visitable result = super.visit(newExpr, p); - className = oldClassName; - return result; - } - - @Override - @SuppressWarnings("EmptyCatch") - public Visitable visit(MarkerAnnotationExpr anno, Void p) { - try { - ResolvedAnnotationDeclaration resolvedAnno = anno.resolve(); - if (!(resolvedAnno instanceof ReflectionAnnotationDeclaration)) { - // ResolvedAnnotationDeclaration means no file/CompilationUnit behind anno - // So, we must still generate it even though it's resolved - return super.visit(anno, p); - } - } catch (UnsolvedSymbolException ex) { - - } catch (ClassCastException ex) { - // A ClassCastException is only raised in specific circumstances; for example, when an - // annotation is an inner class (has a dot) and is used in/on a class which references itself: - /* - @Foo.Bar - public class Test { - Test foo() { - return null; - } - } - */ - // In these cases, a JavaParserClassDeclaration is being casted to a - // ResolvedAnnotationDeclaration, thus causing the error. However, through - // testing, this exception was only raised on subsequent resolve()s (not the - // first), so the synthetic type should already be generated. - return super.visit(anno, p); - } - - UnsolvedClassOrInterface unsolvedAnnotation; - - if (JavaParserUtil.isAClassPath(anno.getNameAsString())) { - @SuppressWarnings("signature") // Already guaranteed to be a FQN here - @FullyQualifiedName String qualifiedTypeName = anno.getNameAsString(); - unsolvedAnnotation = getSimpleSyntheticClassFromFullyQualifiedName(qualifiedTypeName); - updateMissingClass(unsolvedAnnotation); - } else { - unsolvedAnnotation = updateUnsolvedClassWithClassName(anno.getNameAsString(), false, false); - } - - unsolvedAnnotation.setIsAnAnnotationToTrue(); - - return super.visit(anno, p); - } - - @Override - @SuppressWarnings("EmptyCatch") - public Visitable visit(NormalAnnotationExpr anno, Void p) { - try { - ResolvedAnnotationDeclaration resolvedAnno = anno.resolve(); - if (!(resolvedAnno instanceof ReflectionAnnotationDeclaration)) { - // ResolvedAnnotationDeclaration means no file/CompilationUnit behind anno - // So, we must still generate it even though it's resolved - return super.visit(anno, p); - } - return super.visit(anno, p); - } catch (UnsolvedSymbolException ex) { - - } catch (ClassCastException ex) { - // A ClassCastException is only raised in specific circumstances; for example, when an - // annotation is an inner class (has a dot) and is used in/on a class which references itself: - /* - @Foo.Bar - public class Test { - Test foo() { - return null; - } - } - */ - // In these cases, a JavaParserClassDeclaration is being casted to a - // ResolvedAnnotationDeclaration, thus causing the error. However, through - // testing, this exception was only raised on subsequent resolve()s (not the - // first), so the synthetic type should already be generated. - return super.visit(anno, p); - } - - UnsolvedClassOrInterface unsolvedAnnotation; - - if (JavaParserUtil.isAClassPath(anno.getNameAsString())) { - @SuppressWarnings("signature") // Already guaranteed to be a FQN here - @FullyQualifiedName String qualifiedTypeName = anno.getNameAsString(); - unsolvedAnnotation = getSimpleSyntheticClassFromFullyQualifiedName(qualifiedTypeName); - updateMissingClass(unsolvedAnnotation); - } else { - unsolvedAnnotation = updateUnsolvedClassWithClassName(anno.getNameAsString(), false, false); - } - - unsolvedAnnotation.setIsAnAnnotationToTrue(); - - // Add annotation parameters and resolve the annotation parameters to their types - for (MemberValuePair pair : anno.getPairs()) { - unsolvedAnnotation.addMethod( - new UnsolvedMethod( - pair.getNameAsString(), - JavaParserUtil.getValueTypeFromAnnotationExpression(pair.getValue()), - Collections.emptyList(), - true)); - } - - return super.visit(anno, p); - } - - @Override - @SuppressWarnings("EmptyCatch") - public Visitable visit(SingleMemberAnnotationExpr anno, Void p) { - try { - ResolvedAnnotationDeclaration resolvedAnno = anno.resolve(); - if (!(resolvedAnno instanceof ReflectionAnnotationDeclaration)) { - // ResolvedAnnotationDeclaration means no file/CompilationUnit behind anno - // So, we must still generate it even though it's resolved - return super.visit(anno, p); - } - return super.visit(anno, p); - } catch (UnsolvedSymbolException ex) { - - } catch (ClassCastException ex) { - // A ClassCastException is only raised in specific circumstances; for example, when an - // annotation is an inner class (has a dot) and is used in/on a class which references itself: - /* - @Foo.Bar - public class Test { - Test foo() { - return null; - } - } - */ - // In these cases, a JavaParserClassDeclaration is being casted to a - // ResolvedAnnotationDeclaration, thus causing the error. However, through - // testing, this exception was only raised on subsequent resolve()s (not the - // first), so the synthetic type should already be generated. - return super.visit(anno, p); - } - - UnsolvedClassOrInterface unsolvedAnnotation; - - if (JavaParserUtil.isAClassPath(anno.getNameAsString())) { - @SuppressWarnings("signature") // Already guaranteed to be a FQN here - @FullyQualifiedName String qualifiedTypeName = anno.getNameAsString(); - unsolvedAnnotation = getSimpleSyntheticClassFromFullyQualifiedName(qualifiedTypeName); - updateMissingClass(unsolvedAnnotation); - } else { - unsolvedAnnotation = updateUnsolvedClassWithClassName(anno.getNameAsString(), false, false); - } - - unsolvedAnnotation.setIsAnAnnotationToTrue(); - - // Add annotation parameters and resolve the annotation parameters to their types - unsolvedAnnotation.addMethod( - new UnsolvedMethod( - "value", - JavaParserUtil.getValueTypeFromAnnotationExpression(anno.getMemberValue()), - Collections.emptyList(), - true)); - - return super.visit(anno, p); - } - - /** - * Converts a qualified class name into a relative file path. Angle brackets for type variables - * are permitted in the input. - * - * @param qualifiedName The qualified name of the class. - * @return The relative file path corresponding to the qualified name. - */ - public String qualifiedNameToFilePath(String qualifiedName) { - if (!existingClassesToFilePath.containsKey(qualifiedName)) { - throw new RuntimeException( - "qualifiedNameToFilePath only works for classes in the original directory"); - } - Path absoluteFilePath = existingClassesToFilePath.get(qualifiedName); - // theoretically rootDirectory should already be absolute as stated in README. - Path absoluteRootDirectory = Paths.get(rootDirectory).toAbsolutePath(); - return absoluteRootDirectory.relativize(absoluteFilePath).toString(); - } - - /** - * Updates the list of added target files based on a FieldAccessExpr if it represents an enum - * constant. - * - * @param expr the FieldAccessExpr potentially representing an Enum constant. - * @return true if the update was successful, false otherwise. - */ - public boolean updatedAddedTargetFilesForPotentialEnum(FieldAccessExpr expr) { - ResolvedValueDeclaration resolved; - try { - resolved = expr.resolve(); - } catch (UnsolvedSymbolException | UnsupportedOperationException e) { - return false; - } - if (resolved.isEnumConstant()) { - if (JavaLangUtils.inJdkPackage(resolved.getType().describe())) { - return false; - } - String filePathName = qualifiedNameToFilePath(resolved.getType().describe()); - if (!addedTargetFiles.contains(filePathName)) { - gotException(); - addedTargetFiles.add(filePathName); - return true; - } - } - return false; - } - - /** - * Given a ResolvedReferenceType, this method checks if all of the type parameters of that type - * are resolved. - * - * @param resolvedReferenceType a resolved reference type - * @return true if resolvedReferenceType has no unresolved type parameters. - */ - public boolean hasResolvedTypeParameters(ResolvedReferenceType resolvedReferenceType) { - for (Pair typeParameter : - resolvedReferenceType.getTypeParametersMap()) { - ResolvedType parameterType = typeParameter.b; - if (parameterType.isReferenceType()) { - try { - // check if parameterType extends any unresolved type. - parameterType.asReferenceType().getAllAncestors(); - } catch (UnsolvedSymbolException e) { - return false; - } - } - } - return true; - } - - /** - * Given a method call, this method checks if that method call is declared in the currently - * visiting class. - * - * @param method the method call to be checked - * @return true if method is declared in the current clases - */ - public boolean declaredInCurrentClass(MethodCallExpr method) { - for (Set methodDeclarationSet : declaredMethod) { - for (MethodDeclaration methodDeclared : methodDeclarationSet) { - if (!methodDeclared.getName().asString().equals(method.getName().asString())) { - continue; - } - List methodTypesOfArguments = - getArgumentTypesFromMethodCall(method, currentPackage); - NodeList methodDeclaredParameters = methodDeclared.getParameters(); - List methodDeclaredTypesOfParameters = new ArrayList<>(); - for (Parameter parameter : methodDeclaredParameters) { - try { - if (isTypeVar(parameter.getTypeAsString())) { - methodDeclaredTypesOfParameters.add(parameter.getTypeAsString()); - } else { - ResolvedType parameterTypeResolved = parameter.getType().resolve(); - if (parameterTypeResolved.isPrimitive()) { - methodDeclaredTypesOfParameters.add(parameterTypeResolved.asPrimitive().name()); - } else if (parameterTypeResolved.isReferenceType()) { - methodDeclaredTypesOfParameters.add( - parameterTypeResolved.asReferenceType().getQualifiedName()); - } - } - } catch (UnsolvedSymbolException e) { - // UnsolvedSymbolVisitor will not create any synthetic class at this iteration. - return false; - } - } - if (methodDeclaredTypesOfParameters.equals(methodTypesOfArguments)) { - return true; - } - } - } - return false; - } - - /** - * Resolves symbols for a given ClassOrInterfaceType instance, including its type variables if - * present. - * - * @param typeExpr The ClassOrInterfaceType instance to resolve symbols for. - */ - private void solveSymbolsForClassOrInterfaceType( - ClassOrInterfaceType typeExpr, boolean isAnInterface) { - Optional> typeArguments = typeExpr.getTypeArguments(); - UnsolvedClassOrInterface classToUpdate; - int numberOfArguments = 0; - String typeRawName = typeExpr.getElementType().asString(); - Set preferredTypeVariables = new HashSet<>(); - if (typeArguments.isPresent()) { - numberOfArguments = typeArguments.get().size(); - for (Type typeArgument : typeArguments.get()) { - String typeArgStandardForm = typeArgument.toString(); - if (typeArgStandardForm.contains("@")) { - // Remove annotations - List split = List.of(typeArgStandardForm.split("\\s")); - typeArgStandardForm = - split.stream().filter(s -> !s.startsWith("@")).collect(Collectors.joining(" ")); - } - if (typeArgStandardForm.startsWith("? extends ")) { - // there are 10 characters in "? extends ". The idea here is that users sometimes need - // to add a wildcard to annotate a typevar - so "V" might be expressed as "@X ? extends - // V". - typeArgStandardForm = typeArgStandardForm.substring(10); - } - if (isTypeVar(typeArgStandardForm)) { - preferredTypeVariables.add(typeArgStandardForm); - } else if (typeArgument.isClassOrInterfaceType()) { - // If the type argument is not a type variable, then - // it must be a class/interface/etc. Try solving for it. - // If that fails, create a synthetic class, just as we - // would for something directly extended. - try { - typeArgument.resolve(); - } catch (UnsolvedSymbolException e) { - // Assumption: type arguments are not interfaces. This isn't really true, but - // Specimin doesn't have a way to know because the type argument context doesn't - // tell us if this type is an interface or not. - solveSymbolsForClassOrInterfaceType(typeArgument.asClassOrInterfaceType(), false); - } - } - } - if (!preferredTypeVariables.isEmpty() && preferredTypeVariables.size() != numberOfArguments) { - throw new RuntimeException( - "Numbers of type variables are not matching! " - + preferredTypeVariables - + " but expected " - + numberOfArguments - + " because the type arguments are: " - + typeArguments.get() - + " and the in-scope type variables are: " - + typeVariables); - } - // without any type argument - typeRawName = typeRawName.substring(0, typeRawName.indexOf("<")); - } - - String packageName, className; - if (JavaParserUtil.isAClassPath(typeRawName)) { - // Two cases: this could be either an Outer.Inner pair or it could - // be a fully-qualified name. If it's an Outer.Inner pair, we identify - // that via the heuristic that there are only two elements if we split on - // the dot and that the whole string is capital - if (typeRawName.indexOf('.') == typeRawName.lastIndexOf('.') - && JavaParserUtil.isCapital(typeRawName)) { - className = typeRawName; - packageName = getPackageFromClassName(typeRawName.substring(0, typeRawName.indexOf('.'))); - } else { - packageName = typeRawName.substring(0, typeRawName.lastIndexOf(".")); - className = typeRawName.substring(typeRawName.lastIndexOf(".") + 1); - } - } else { - className = typeRawName; - packageName = getPackageFromClassName(className); - } - - if (isTypeVar(className)) { - // don't create synthetic classes for in-scope type variables - return; - } - - classToUpdate = - new UnsolvedClassOrInterface( - className, packageName, isInsideCatchBlockParameter, isAnInterface); - - classToUpdate.setNumberOfTypeVariables(numberOfArguments); - classToUpdate.setPreferedTypeVariables(preferredTypeVariables); - - updateMissingClass(classToUpdate); - } - - /** - * Given a field access expression, this method determines whether the field is declared in one of - * the original class file in the codebase (instead of a synthetic class). - * - * @param node a FieldAccessExpr instance - * @return true if the field is inside an original class file - */ - public boolean belongsToARealClassFile(FieldAccessExpr node) { - Expression nodeScope = node.getScope(); - return existingClassesToFilePath.containsKey(nodeScope.calculateResolvedType().describe()); - } - - /** - * Given the variable type and the basic declaration of that variable (such as "int x", "boolean - * y", "Car redTruck",...), this methods will add an initial value to that declaration of the - * variable. The way the initial value is chosen is based on the document of the Java Language: - * https://docs.oracle.com/javase/specs/jls/se7/html/jls-4.html#jls-4.12.5 - * - * @param variableType the type of the variable - * @param variableDeclaration the basic declaration of that variable - * @return the declaration of the variable with an initial value - */ - public static String setInitialValueForVariableDeclaration( - String variableType, String variableDeclaration) { - return variableDeclaration + " = " + getInitializerRHS(variableType); - } - - /** - * Returns a type-compatible initializer for a field of the given type. - * - * @param variableType the type of the field - * @return a type-compatible initializer - */ - private static String getInitializerRHS(String variableType) { - switch (variableType) { - case "byte": - return "(byte)0"; - case "short": - return "(short)0"; - case "int": - return "0"; - case "long": - return "0L"; - case "float": - return "0.0f"; - case "double": - return "0.0d"; - case "char": - return "'\\u0000'"; - case "boolean": - return "false"; - default: - return "null"; - } - } - - /** - * Updates the list of target files if the given type extends another class or interface and its - * class file is present in the original codebase. - * - *

Note: this method only updates the list of target files if the inheritance is resolved. - * - * @param classOrInterfaceType A type that may have inheritance. - * @return True if the updating process was successful; otherwise, false. - */ - private boolean updateTargetFilesListForExistingClassWithInheritance( - ClassOrInterfaceType classOrInterfaceType) { - String classSimpleName = classOrInterfaceType.getNameAsString(); - String fullyQualifiedName = getPackageFromClassName(classSimpleName) + "." + classSimpleName; - - if (!classfileIsInOriginalCodebase(fullyQualifiedName)) { - return false; - } - - ResolvedReferenceType resolvedClass; - try { - // since resolvedClass is a ClassOrInterfaceType instance, it is safe to cast it to a - // ReferenceType. - resolvedClass = - JavaParserUtil.classOrInterfaceTypeToResolvedReferenceType(classOrInterfaceType); - if (!resolvedClass.getAllAncestors().isEmpty()) { - String pathOfThisCurrentType = qualifiedNameToFilePath(fullyQualifiedName); - if (!addedTargetFiles.contains(pathOfThisCurrentType)) { - addedTargetFiles.add(pathOfThisCurrentType); - gotException(); - } - return true; - } - return false; - } catch (UnsolvedSymbolException | UnsupportedOperationException e) { - return false; - } - } - - /** - * Given an instance of MethodCallExpr, this method checks if that method has no scope or called - * by the "this" keyword. - * - * @param methodCall the method call to be checked. - * @return true if methodCall has no scope or called by a "this" keyword. - */ - private boolean haveNoScopeOrCallByThisKeyword(MethodCallExpr methodCall) { - Optional scope = methodCall.getScope(); - if (scope.isEmpty()) { - return true; - } - return (scope.get() instanceof ThisExpr); - } - - /** - * Given a node, this method checks if that node is inside an object creation expression (meaning - * that it belongs to an anonymous class). - * - * @param node a node - * @return true if node is inside an object creation expression - */ - private boolean insideAnObjectCreation(Node node) { - while (node.getParentNode().isPresent()) { - Node parent = node.getParentNode().get(); - if (parent instanceof ObjectCreationExpr) { - return true; - } - if (parent instanceof ClassOrInterfaceDeclaration) { - return false; - } - if (parent instanceof EnumConstantDeclaration) { - return false; - } - if (parent instanceof EnumDeclaration) { - return false; - } - node = parent; - } - throw new RuntimeException("Got a node with no containing class!"); - } - - /** - * Given a type variable, update the list of synthetic classes accordingly. Node: while the type - * of the input for this method is ClassOrInterfaceType, it is actually a type variable. Make sure - * to check with {@link UnsolvedSymbolVisitor#isTypeVar(String)} before calling this method. - * - * @param type a type variable to be used as input. - */ - private void updateSyntheticClassesForTypeVar(ClassOrInterfaceType type) { - String typeSimpleName = type.getNameAsString(); - for (Map> typeScope : typeVariables) { - if (typeScope.containsKey(typeSimpleName)) { - NodeList boundOfType = typeScope.get(typeSimpleName); - for (int index = 0; index < boundOfType.size(); index++) { - try { - boundOfType.get(index).resolve(); - } catch (UnsolvedSymbolException | UnsupportedOperationException e) { - if (e instanceof UnsolvedSymbolException) { - this.gotException(); - // quoted from the documentation of Oracle: "A type variable with multiple bounds is a - // subtype of all the types listed in the bound. If one of the bounds is a class, it - // must be specified first." - // If the first bound is also unsolved, it is better to assume it to be a class. - boolean shouldBeAnInterface = !(index == 0); - solveSymbolsForClassOrInterfaceType(boundOfType.get(index), shouldBeAnInterface); - } - } - } - } - } - } - - /** - * Given a class name that can either be fully-qualified or simple, this method will convert that - * class name to a simple name. - * - * @param className the class name to be converted - * @return the simple form of that class name - */ - // We can have certainty that this method is true as the last element of a class name is the - // simple form of that name - @SuppressWarnings("signature") - public static @ClassGetSimpleName String toSimpleName(@DotSeparatedIdentifiers String className) { - List elements = Splitter.onPattern("[.]").splitToList(className); - if (elements.size() < 2) { - return className; - } - return elements.get(elements.size() - 1); - } - - /** - * This method will add a new method declaration to a synthetic class based on the unsolved method - * call or method declaration in the original input. User can choose the desired return type for - * the added method. The desired return type can be an empty string, and in that case, Specimin - * will create another synthetic class to be the return type of that method. - * - * @param method the method call or method declaration in the original input - * @param className the name of the synthetic class, which may be either simple or fully-qualified - * @param desiredReturnType the desired return type for this method - * @param updatingInterface true if this method is being used to update an interface, false for - * updating classes - */ - public void updateUnsolvedClassOrInterfaceWithMethod( - Node method, String className, String desiredReturnType, boolean updatingInterface) { - String methodName = ""; - List listOfParameters = new ArrayList<>(); - List listOfExceptions = new ArrayList<>(); - String accessModifer = "public"; - if (method instanceof MethodCallExpr) { - methodName = ((MethodCallExpr) method).getNameAsString(); - String packageName = splitName(className).a; - listOfParameters = getArgumentTypesFromMethodCall(((MethodCallExpr) method), packageName); - } - // method is a MethodDeclaration - else { - MethodDeclaration methodDecl = (MethodDeclaration) method; - - methodName = methodDecl.getNameAsString(); - accessModifer = methodDecl.getAccessSpecifier().asString(); - for (Parameter para : methodDecl.getParameters()) { - Type paraType = para.getType(); - String paraTypeAsString = paraType.asString(); - try { - // if possible, opt for fully-qualified names. - ResolvedType resolvedType = paraType.resolve(); - paraTypeAsString = resolvedType.describe(); - } catch (UnsolvedSymbolException | UnsupportedOperationException e) { - // avoiding ignored catch blocks errors. - listOfParameters.add(paraTypeAsString); - continue; - } - listOfParameters.add(paraTypeAsString); - } - - for (ReferenceType exception : methodDecl.getThrownExceptions()) { - String exceptionTypeAsString = exception.asString(); - try { - // if possible, opt for fully-qualified names. - exceptionTypeAsString = exception.resolve().describe(); - } catch (UnsolvedSymbolException | UnsupportedOperationException e) { - // avoiding ignored catch blocks errors. - listOfExceptions.add(exceptionTypeAsString); - continue; - } - listOfExceptions.add(exceptionTypeAsString); - } - } - if (listOfParameters.contains(JavaTypeCorrect.SYNTHETIC_UNCONSTRAINED_TYPE)) { - // return early: this method is an artifact of JavaTypeCorrect and won't be needed. - return; - } - String returnType = ""; - if (desiredReturnType.equals("")) { - returnType = returnNameForMethod(methodName); - } else { - returnType = desiredReturnType; - } - - UnsolvedMethod thisMethod = - new UnsolvedMethod( - methodName, - returnType, - listOfParameters, - updatingInterface, - accessModifer, - listOfExceptions); - - if (method instanceof MethodCallExpr) { - updateUnsolvedMethodsWithMethodReferences(method, thisMethod); - } - - UnsolvedClassOrInterface missingClass = - updateUnsolvedClassWithClassName(className, false, false, thisMethod); - syntheticMethodReturnTypeAndClass.put(returnType, missingClass); - - // if the return type is not specified, a synthetic return type will be created. This part of - // codes creates the corresponding class for that synthetic return type - if (desiredReturnType.equals("")) { - UnsolvedClassOrInterface returnTypeForThisMethod = - new UnsolvedClassOrInterface(returnType, missingClass.getPackageName()); - this.updateMissingClass(returnTypeForThisMethod); - classAndPackageMap.put( - returnTypeForThisMethod.getClassName(), returnTypeForThisMethod.getPackageName()); - } - } - - /** - * Processes a MethodDeclaration by creating necessary synthetic classes for the declaration to be - * resolved and updating the records of local variables and type variables accordingly. This - * method also visits that MethodDeclaration input. - * - * @param node The MethodDeclaration to be used as input. - * @return A Visitable object representing the MethodDeclaration. - */ - public Visitable processMethodDeclaration(MethodDeclaration node) { - // a MethodDeclaration instance will have parent node - Node parentNode = node.getParentNode().get(); - Type nodeType = node.getType(); - - addTypeVariableScope(node.getTypeParameters()); - - // since this is a return type of a method, it is a dot-separated identifier - @SuppressWarnings("signature") - @DotSeparatedIdentifiers String nodeTypeAsString = nodeType.asString(); - @ClassGetSimpleName String nodeTypeSimpleForm = toSimpleName(nodeTypeAsString); - - if (!insideAnObjectCreation(node)) { - SimpleName classNodeSimpleName = getSimpleNameOfClass(node); - className = classNodeSimpleName.asString(); - methodAndReturnType.put(node.getNameAsString(), nodeTypeSimpleForm); - } - // node is a method declaration inside an anonymous class - else { - try { - // since this method declaration is inside an anonymous class, its parent will be an - // ObjectCreationExpr - ((ObjectCreationExpr) parentNode).resolve(); - } catch (UnsolvedSymbolException | UnsupportedOperationException e) { - SimpleName classNodeSimpleName = ((ObjectCreationExpr) parentNode).getType().getName(); - String nameOfClass = classNodeSimpleName.asString(); - updateUnsolvedClassOrInterfaceWithMethod( - node, nameOfClass, toSimpleName(nodeTypeAsString), false); - } - } - - // These are two places where a checked exception can appear, in a catch phrase or in the - // declaration of a method. This part handles the second case. - for (ReferenceType throwType : node.getThrownExceptions()) { - try { - throwType.resolve(); - } catch (UnsolvedSymbolException | UnsupportedOperationException e) { - String typeName = throwType.asString(); - UnsolvedClassOrInterface typeOfThrow = - new UnsolvedClassOrInterface(typeName, getPackageFromClassName(typeName)); - typeOfThrow.extend("java.lang.Throwable"); - updateMissingClass(typeOfThrow); - } - } - - // if the second condition is false, then this method belongs to an anonymous class, which - // should be handled by the codes above. - if (node.isAnnotationPresent("Override") && classAndItsParent.containsKey(className)) { - String parentClassName = classAndItsParent.get(className); - // A modular program analysis can reason about @Override, hence we need to create a synthetic - // version for the overriden method if missing. - - // TODO: Tracing the complete inheritance tree to locate the missing class is more ideal. - // However, due to the limitations of JavaParser, we're unable to inspect each ancestor - // independently. Instead, we can only obtain the resolved versions of all ancestors of a - // resolved type at once. If any ancestor remains unresolved, we end up with a not very useful - // exception. - if (!classfileIsInOriginalCodebase(parentClassName)) { - if (nodeType.isReferenceType()) { - updateUnsolvedClassOrInterfaceWithMethod( - node, - parentClassName, - getPackageFromClassName(nodeTypeSimpleForm) + "." + nodeTypeSimpleForm, - false); - } else { - updateUnsolvedClassOrInterfaceWithMethod( - node, parentClassName, nodeTypeSimpleForm, false); - } - } - } - - Set currentLocalVariables = getParameterFromAMethodDeclaration(node); - localVariables.addFirst(currentLocalVariables); - Visitable result = super.visit(node, null); - localVariables.removeFirst(); - typeVariables.removeFirst(); - return result; - } - - /** - * Checks if the given expression is solvable because the class file containing its symbol is - * found in one of the jar files provided via the {@code --jarPath} option. - * - * @param expr The expression to be checked for solvability. - * @return true iff the expression is solvable because its class file was found in one of the jar - * files. - */ - public boolean isFromAJarFile(Expression expr) { - String className; - if (expr instanceof MethodCallExpr) { - try { - className = - ((MethodCallExpr) expr).resolve().getPackageName() - + "." - + ((MethodCallExpr) expr).resolve().getClassName(); - } catch (UnsupportedOperationException e) { - // This is a limitation of JavaParser. If a method call has a generic return type, sometimes - // JavaParser can not resolve it. - // The consequence is that we can not get the class where a method is declared if that - // method has a generic return type. Hopefully the later version of JavaParser can address - // this limitation. - return false; - } - } else if (expr instanceof ObjectCreationExpr) { - String shortName = ((ObjectCreationExpr) expr).getTypeAsString(); - String packageName = classAndPackageMap.get(shortName); - className = packageName + "." + shortName; - } else { - throw new RuntimeException("Unexpected call: " + expr + ". Contact developers!"); - } - return classesFromJar.contains(className); - } - - /** - * This method updates a synthetic file based on a solvable expression. The input expression is - * solvable because its data is in the jar files that Specimin takes as input. - * - * @param expr the expression to be used - */ - public void updateClassesFromJarSourcesForMethodCall(MethodCallExpr expr) { - if (!isFromAJarFile(expr)) { - throw new RuntimeException( - "Check with isFromAJarFile first before using updateClassesFromJarSources"); - } - String methodName = expr.getNameAsString(); - ResolvedMethodDeclaration methodSolved = expr.resolve(); - @SuppressWarnings( - "signature") // this is not a precise assumption, as getClassName() will return a - // @FullyQualifiedName if the class is not of primitive type. However, this is - // favorable, since we don't have to write any additional import statements. - @ClassGetSimpleName String className = methodSolved.getClassName(); - String packageName = methodSolved.getPackageName(); - @SuppressWarnings( - "signature") // this is not a precise assumption, as getReturnType().describe() will return - // a @FullyQualifiedName if the class is not of primitive type. However, this - // is favorable, since we don't have to write any additional import statements. - @ClassGetSimpleName String returnType = methodSolved.getReturnType().describe(); - List argumentsList = getArgumentTypesFromMethodCall(expr, packageName); - UnsolvedClassOrInterface missingClass = new UnsolvedClassOrInterface(className, packageName); - UnsolvedMethod thisMethod = new UnsolvedMethod(methodName, returnType, argumentsList); - - updateUnsolvedMethodsWithMethodReferences(expr, thisMethod); - - missingClass.addMethod(thisMethod); - syntheticMethodReturnTypeAndClass.put(returnType, missingClass); - this.updateMissingClass(missingClass); - } - - /** - * Given the simple name of an unsolved class, this method will create an UnsolvedClass instance - * to represent that class and update the list of missing class with that UnsolvedClass instance. - * - * @param nameOfClass the name of an unsolved class. This could be a simple name, but it may also - * contain scoping constructs for outer classes. For example, it could be "Outer.Inner". - * Alternatively, it may already be a fully-qualified name. - * @param unsolvedMethods unsolved methods to add to the class before updating this visitor's set - * missing classes (optional, may be omitted) - * @param isExceptionType if the class is of exceptionType - * @param isUpdatingInterface indicates whether this method is being used to update an interface - * @return the newly-created UnsolvedClass method, for further processing. This output may be - * ignored. - */ - public UnsolvedClassOrInterface updateUnsolvedClassWithClassName( - String nameOfClass, - boolean isExceptionType, - boolean isUpdatingInterface, - UnsolvedMethod... unsolvedMethods) { - // If the name of the class is not present among import statements, we assume that this unsolved - // class is in the same directory as the current class. - - Pair packageAndClassNames = splitName(nameOfClass); - UnsolvedClassOrInterface result; - result = - new UnsolvedClassOrInterface( - packageAndClassNames.b, packageAndClassNames.a, isExceptionType, isUpdatingInterface); - for (UnsolvedMethod unsolvedMethod : unsolvedMethods) { - result.addMethod(unsolvedMethod); - } - updateMissingClass(result); - return result; - } - - /** - * Splits a name into the package and class names, based on the upper/lower case heuristic. - * - * @param name a simple name, a fully-qualified name, or an inner class name like Outer.Inner - * @return a pair whose first element is the package name and whose second element is the class - * name - */ - private Pair splitName(String name) { - String packageName = "", simpleClassName = ""; - - // Four cases based on these examples: org.pkg.Simple, org.pkg.Simple.Inner, Simple, - // Simple.Inner - // Using a heuristic for checking for an FQN: that package names start with lower-case letters, - // and class names start with upper-class letters. - if (Character.isLowerCase(name.charAt(0))) { - // original name assumed to have been fully-qualified - Iterable parts = Splitter.on('.').split(name); - for (String part : parts) { - if (Character.isLowerCase(part.charAt(0))) { - if ("".equals(packageName)) { - packageName = part; - } else { - packageName += "." + part; - } - } else { - if ("".equals(simpleClassName)) { - simpleClassName = part; - } else { - simpleClassName += "." + part; - } - } - } - } else { - // original name assumed to have been simple (but might have inner classes) - String scope = name.indexOf('.') == -1 ? name : name.substring(0, name.indexOf('.')); - // If the class name is not purely simple, use the outermost scope. - packageName = getPackageFromClassName(scope); - simpleClassName = name; - } - return new Pair(packageName, simpleClassName); - } - - /** - * This method updates a synthetic file based on a solvable expression. The input expression is - * solvable because its data is in the jar files that Specimin taks as input. - * - * @param expr the expression to be used - */ - public void updateClassesFromJarSourcesForObjectCreation(ObjectCreationExpr expr) { - if (!isFromAJarFile(expr)) { - throw new RuntimeException( - "Check with isFromAJarFile first before using updateClassesFromJarSources"); - } - String objectName = expr.getType().getName().asString(); - ResolvedReferenceTypeDeclaration objectSolved = expr.resolve().declaringType(); - @SuppressWarnings( - "signature") // this is not a precise assumption, as getClassName() will return a - // @FullyQualifiedName if the class is not of primitive type. However, this is - // favorable, since we don't have to write any additional import statements. - @ClassGetSimpleName String className = objectSolved.getClassName(); - String packageName = objectSolved.getPackageName(); - List argumentsList = getArgumentTypesFromObjectCreation(expr, packageName); - UnsolvedClassOrInterface missingClass = new UnsolvedClassOrInterface(className, packageName); - UnsolvedMethod thisMethod = new UnsolvedMethod(objectName, "", argumentsList); - - updateUnsolvedMethodsWithMethodReferences(expr, thisMethod); - - missingClass.addMethod(thisMethod); - this.updateMissingClass(missingClass); - } - - /** - * This method checks if an expression is called by the super keyword. For example, super.visit() - * is such an expression. - * - * @param node the expression to be checked - * @return true if method is a super call - */ - public boolean isASuperCall(Expression node) { - if (node instanceof MethodCallExpr) { - Optional caller = node.asMethodCallExpr().getScope(); - if (caller.isEmpty()) { - return false; - } - return caller.get().isSuperExpr(); - } else if (node instanceof FieldAccessExpr) { - Expression caller = node.asFieldAccessExpr().getScope(); - String fieldName = ((FieldAccessExpr) node).getNameAsString(); - return caller.isSuperExpr() - || (caller.isThisExpr() && !fieldNameToClassNameMap.containsKey(fieldName)); - } else if (node instanceof NameExpr) { - // an unsolved name expression implies that it is declared in the parent class - return !canBeSolved(node); - } else { - throw new RuntimeException("Unforeseen expression: " + node); - } - } - - /** - * Given a method declaration, this method will return the set of parameters of that method - * declaration. - * - * @param decl the method declaration - * @return the set of parameters of decl - */ - public Set getParameterFromAMethodDeclaration(MethodDeclaration decl) { - Set setOfParameters = new HashSet<>(); - for (Parameter parameter : decl.getParameters()) { - setOfParameters.add(parameter.getName().asString()); - } - return setOfParameters; - } - - /** - * Given a non-static and unsolved field access expression, this method will update the - * corresponding synthetic class. - * - * @param field a non-static field access expression - */ - public void updateSyntheticClassWithNonStaticFields(FieldAccessExpr field) { - Expression caller = field.getScope(); - String fullyQualifiedClassName = caller.calculateResolvedType().describe(); - int indexOfAngleBracket = fullyQualifiedClassName.indexOf('<'); - if (indexOfAngleBracket != -1) { - fullyQualifiedClassName = fullyQualifiedClassName.substring(0, indexOfAngleBracket); - } - String fieldQualifedSignature = fullyQualifiedClassName + "." + field.getNameAsString(); - updateClassSetWithQualifiedFieldSignature(fieldQualifedSignature, false, false); - } - - /** - * For a super call, this method will update the corresponding synthetic class - * - * @param expr the super call expression to be taken as input - */ - public void updateSyntheticClassForSuperCall(Expression expr) { - if (!isASuperCall(expr)) { - throw new RuntimeException( - "Check if isASuperCall returns true before calling updateSyntheticClassForSuperCall"); - } - // If we're inside an object creation, this is an anonymous class. Locate any super things - // in the class that's being extended. - - String parentClassName; - try { - parentClassName = insideAnObjectCreation(expr) ? className : getParentClass(className); - } catch (RuntimeException e) { - throw new RuntimeException("crashed while trying to get the parent for " + expr, e); - } - if (expr instanceof MethodCallExpr) { - updateUnsolvedClassOrInterfaceWithMethod( - expr.asMethodCallExpr(), - parentClassName, - methodAndReturnType.getOrDefault(expr.asMethodCallExpr().getNameAsString(), ""), - false); - } else if (expr instanceof FieldAccessExpr) { - String nameAsString = expr.asFieldAccessExpr().getNameAsString(); - updateUnsolvedSuperClassWithFields( - nameAsString, parentClassName, getPackageFromClassName(parentClassName)); - } else if (expr instanceof NameExpr) { - String nameAsString = expr.asNameExpr().getNameAsString(); - updateUnsolvedSuperClassWithFields( - nameAsString, parentClassName, getPackageFromClassName(parentClassName)); - } else { - throw new RuntimeException("Unexpected expression: " + expr); - } - } - - /** - * This method will add a new field declaration to a synthetic class. This method is intended to - * be used for unsolved superclass. The declaration of the field in the superclass will be the - * same as the declaration in the child class since Specimin does not have access to much - * information. If the field is not found in the child class, Specimin will create a synthetic - * class to be the type of that field. - * - * @param var the field to be added - * @param className the name of the synthetic class - * @param packageName the package of the synthetic class - */ - public void updateUnsolvedSuperClassWithFields( - String var, @ClassGetSimpleName String className, String packageName) { - UnsolvedClassOrInterface relatedClass = new UnsolvedClassOrInterface(className, packageName); - if (variablesAndDeclaration.containsKey(var)) { - String variableExpression = variablesAndDeclaration.get(var); - relatedClass.addFields(variableExpression); - updateMissingClass(relatedClass); - } else { - // since it is just simple string combination, it is a simple name - @SuppressWarnings("signature") - @ClassGetSimpleName String variableType = "SyntheticTypeFor" + toCapital(var); - UnsolvedClassOrInterface varType = - new UnsolvedClassOrInterface(variableType, getPackageFromClassName(variableType)); - relatedClass.addFields( - setInitialValueForVariableDeclaration( - variableType, varType.getQualifiedClassName() + " " + var)); - updateMissingClass(relatedClass); - updateMissingClass(varType); - } - } - - /** - * This method checks if a variable is local. - * - * @param variableName the name of the variable - * @return true if that variable is local - */ - public boolean isALocalVar(String variableName) { - for (Set varSet : localVariables) { - // for anonymous classes, it is assumed that any matching local variable either belongs to the - // class itself or is a final variable in the enclosing scope. - if (varSet.contains(variableName)) { - return true; - } - } - return false; - } - - /** - * Is the given type name actually an in-scope type variable? - * - * @param typeName a simple name of a type, as written in a source file. The type name might be an - * in-scope type variable. - * @return true iff there is a type variable in scope with this name. Returning false guarantees - * that there is no such type variable, but not that the input is a valid type. - */ - private boolean isTypeVar(String typeName) { - for (Map> scope : typeVariables) { - if (scope.containsKey(typeName)) { - return true; - } - } - return false; - } - - /** - * Adds a scope with the given list of type parameters. Each pair to this method must be paired - * with a call to typeVariables.removeFirst(). - * - * @param typeParameters a list of type parameters - */ - private void addTypeVariableScope(List typeParameters) { - Map> typeVariableScope = new HashMap<>(); - for (TypeParameter t : typeParameters) { - typeVariableScope.put(t.getNameAsString(), t.getTypeBound()); - } - typeVariables.addFirst(typeVariableScope); - } - - /** - * Given a method declaration, this method will get the name of the class in which the method was - * declared. - * - * @param node the method declaration for input. - * @return the name of the class to which that declaration belongs. - */ - private SimpleName getSimpleNameOfClass(MethodDeclaration node) { - SimpleName classNodeSimpleName; - Node parentNode = node.getParentNode().get(); - if (parentNode instanceof EnumConstantDeclaration) { - classNodeSimpleName = ((EnumDeclaration) parentNode.getParentNode().get()).getName(); - } else if (parentNode instanceof EnumDeclaration) { - classNodeSimpleName = ((EnumDeclaration) parentNode).getName(); - } else if (parentNode instanceof ClassOrInterfaceDeclaration) { - classNodeSimpleName = ((ClassOrInterfaceDeclaration) parentNode).getName(); - } else { - throw new RuntimeException("Unexpected parent node: " + parentNode); - } - return classNodeSimpleName; - } - - /** - * This method checks if the current run of UnsolvedSymbolVisitor can solve the types of the - * arguments of a method call or similar structure (e.g., constructor invocation) - * - * @param argList the arguments to check - * @return true if UnsolvedSymbolVisitor can solve the types of parameters of method-like - */ - public static boolean canSolveArguments(NodeList argList) { - if (argList.isEmpty()) { - return true; - } - for (Expression arg : argList) { - if (arg.isLambdaExpr() || arg.isMethodReferenceExpr()) { - // Skip lambdas and method refs here and treat them specially later. - continue; - } - if (!canBeSolved(arg)) { - return false; - } - } - return true; - } - - /** - * Given a method call, this method returns the list of types of the parameters of that method - * - * @param method the method to be analyzed - * @param pkgName the name of the package of the class that contains the method being called. This - * is only used when creating a functional interface if one of the parameters is a lambda. If - * this argument is null, then this method throws if it encounters a lambda. - * @return the types of parameters of method - */ - public List getArgumentTypesFromMethodCall( - MethodCallExpr method, @Nullable String pkgName) { - NodeList argList = method.getArguments(); - return getArgumentTypesImpl(argList, pkgName); - } - - /** - * Given a ClassOrInterfaceType, this method returns the qualifed name of that type. - * - * @param type a ClassOrInterfaceType instance. - * @return the qualifed name of type - */ - public String getQualifiedNameForClassOrInterfaceType(ClassOrInterfaceType type) { - String typeAsString = type.toString(); - if (typeAsString.contains("<")) { - typeAsString = typeAsString.substring(0, typeAsString.indexOf("<")); - } - String typeSimpleName = type.getName().asString(); - if (!typeAsString.equals(typeSimpleName)) { - // check for inner classes. - List splitType = Splitter.on('.').splitToList(typeAsString); - if (splitType.size() > 2) { - // if the above conditions are met, this type is probably already in the qualified form. - return typeAsString; - } else if (JavaParserUtil.isCapital(typeAsString)) { - // Heuristic: if the type name has two dot-separated components and - // the first one is capitalized, then it's probably an inner class. - // Return the outer class' package. - String outerClass = splitType.get(0); - return getPackageFromClassName(outerClass) + "." + typeAsString; - } - } - return getPackageFromClassName(typeSimpleName) + "." + typeSimpleName; - } - - /** - * Given a new object creation, this method returns the list of types of the parameters of that - * call - * - * @param creationExpr the object creation call - * @param pkgName the name of the package of the class that contains the constructor being called. - * This is only used when creating a functional interface if one of the parameters is a - * lambda. If this argument is null, then this method throws if it encounters a lambda. - * @return the types of parameters of the object creation method - */ - public List getArgumentTypesFromObjectCreation( - ObjectCreationExpr creationExpr, @Nullable String pkgName) { - NodeList argList = creationExpr.getArguments(); - return getArgumentTypesImpl(argList, pkgName); - } - - /** - * Returns all the method references in an argument list. - * - * @param args the argument list - * @return a map of the parameter index to the method reference. - */ - private Map getMethodReferencesFromArguments( - NodeList args) { - Map methodRefs = new HashMap<>(); - for (int i = 0; i < args.size(); i++) { - Expression argument = args.get(i); - if (argument.isMethodReferenceExpr()) { - methodRefs.put(i, argument.asMethodReferenceExpr()); - } - } - return methodRefs; - } - - /** - * Shared implementation for getting argument types from method calls or calls to constructors. - * - * @param argList list of arguments - * @param pkgName the name of the package of the class that contains the method being called. This - * is only used when creating a functional interface if one of the parameters is a lambda or - * method reference. If this argument is null, then this method throws if it encounters a - * lambda/method reference - * @return the list of argument types - */ - private List getArgumentTypesImpl( - NodeList argList, @Nullable String pkgName) { - List parametersList = new ArrayList<>(); - for (Expression arg : argList) { - // Special case for lambdas: don't try to resolve their type, - // and instead compute their arity and provide an appropriate - // functional interface from java.util.function. - if (arg.isLambdaExpr()) { - if (pkgName == null) { - throw new RuntimeException("encountered a lambda when the package name was unknown"); - } - LambdaExpr lambda = arg.asLambdaExpr(); - parametersList.add(resolveLambdaType(lambda, pkgName)); - continue; - } else if (arg.isMethodReferenceExpr()) { - if (pkgName == null) { - throw new RuntimeException( - "encountered a method reference when the package name was unknown"); - } - parametersList.add(resolveMethodExprType(arg.asMethodReferenceExpr(), pkgName)); - continue; - } - - ResolvedType type = arg.calculateResolvedType(); - // for reference type, we need the fully-qualified name to avoid having to add additional - // import statements. - if (type.isReferenceType()) { - ResolvedReferenceType rrType = type.asReferenceType(); - // check if the type isn't public, in which case we shouldn't use - // it as a parameter (it will cause compilation problems) - Optional maybeDecl = rrType.getTypeDeclaration(); - if (maybeDecl.isPresent()) { - ResolvedReferenceTypeDeclaration decl = maybeDecl.get(); - if (decl.isClass()) { - AccessSpecifier access = decl.asClass().accessSpecifier(); - if (access != AccessSpecifier.PUBLIC) { - parametersList.add("java.lang.Object"); - continue; - } - } - } - // avoid creating methods with raw parameter types - int ctypevar = rrType.getTypeParametersMap().size(); - String typevars = ""; - if (ctypevar != 0) { - typevars = - "<" - + String.join(", ", Collections.nCopies(ctypevar, "?").toArray(new String[0])) - + ">"; - } - parametersList.add(rrType.getQualifiedName() + typevars); - } else if (type.isPrimitive()) { - parametersList.add(type.describe()); - } else if (type.isArray()) { - parametersList.add(type.asArrayType().describe()); - } else if (type.isNull()) { - // No way to know what the type should be, so use top. - parametersList.add("java.lang.Object"); - } else if (type.isTypeVariable()) { - parametersList.add(type.asTypeVariable().describe()); - } - // TODO: should we raise an exception here if there is some other kind of type? Could - // any other type (e.g., a type variable) possibly flow here? I think it's possible. - } - return parametersList; - } - - /** - * Returns the correct functional interface for a method reference, or - * java.util.function.Supplier if unresolvable. - * - * @param methodReference the method reference to solve - * @param pkgName the package name to create a synthetic functional interface in, if needed - * @return the fully qualified name of the corresponding functional interface - */ - private String resolveMethodExprType(MethodReferenceExpr methodReference, String pkgName) { - ResolvedMethodDeclaration methodDeclaration; - try { - methodDeclaration = methodReference.resolve(); - } catch (UnsolvedSymbolException ex) { - // Placeholder value; JavaTypeCorrect will update the return and parameter types - return "java.util.function.Supplier"; - } - - int paramCount = methodDeclaration.getNumberOfParams(); - boolean isVoid = methodDeclaration.getReturnType().isVoid(); - - return resolveFunctionalInterface(paramCount, isVoid, pkgName); - } - - /** - * Resolves a type for a lambda expression, possibly by creating a new functional interface. - * - * @param lambda the lambda expression - * @param pkgName the package in which a new functional interface should be created, if necessary - * @return the fully-qualified name of a functional interface that is in-scope and is a supertype - * of the given lambda, according to javac's arity-based typechecking rules for functions - */ - private String resolveLambdaType(LambdaExpr lambda, String pkgName) { - int cparam = lambda.getParameters().size(); - boolean isvoid = isLambdaVoidReturn(lambda); - // we need to run at least once more to solve the functional interface we're about to create - this.gotException(); - - return resolveFunctionalInterface(cparam, isvoid, pkgName); - } - - /** - * Determines if a lambda has a void return. - * - * @param lambda a lambda expression - * @return true iff the lambda has a void return - */ - private boolean isLambdaVoidReturn(LambdaExpr lambda) { - if (lambda.getExpressionBody().isPresent()) { - return false; - } - BlockStmt body = lambda.getBody().asBlockStmt(); - return body.stream().noneMatch(node -> node instanceof ReturnStmt); - } - - /** - * Resolves a functional interface type, given the number of parameters and the presence/absence - * of a return type. - * - * @param numberOfParams the number of parameters - * @param isVoid true iff the method is void - * @param pkgName the package in which a new functional interface should be created, if necessary - * @return the fully-qualified name of a functional interface that is in-scope, matches the - * specified arity, and the specified voidness - */ - private String resolveFunctionalInterface(int numberOfParams, boolean isVoid, String pkgName) { - // we need to run at least once more to solve the functional interface we're about to create - this.gotException(); - // check arity: - if (numberOfParams == 0 && isVoid) { - return "java.lang.Runnable"; - } else if (numberOfParams == 0 && !isVoid) { - return "java.util.function.Supplier"; - } else if (numberOfParams == 1 && isVoid) { - return "java.util.function.Consumer"; - } else if (numberOfParams == 1 && !isVoid) { - return "java.util.function.Function"; - } else if (numberOfParams == 2 && isVoid) { - return "java.util.function.BiConsumer"; - } else if (numberOfParams == 2 && !isVoid) { - return "java.util.function.BiFunction"; - } else { - String funcInterfaceName = - isVoid ? "SyntheticConsumer" + numberOfParams : "SyntheticFunction" + numberOfParams; - UnsolvedClassOrInterface funcInterface = - new UnsolvedClassOrInterface(funcInterfaceName, pkgName, false, true); - int ctypeVars = numberOfParams + (isVoid ? 0 : 1); - funcInterface.setNumberOfTypeVariables(ctypeVars); - String[] paramArray = funcInterface.getTypeVariablesAsStringWithoutBrackets().split(", "); - List params = List.of(paramArray); - if (!isVoid) { - // remove the last element of params, because that's the return type, not a parameter - params = params.subList(0, params.size() - 1); - } - String returnType = isVoid ? "void" : "T" + numberOfParams; - UnsolvedMethod apply = new UnsolvedMethod("apply", returnType, params, true); - funcInterface.addMethod(apply); - updateMissingClass(funcInterface); - - StringBuilder typeArgs = new StringBuilder(); - typeArgs.append("<"); - for (int i = 0; i < ctypeVars; ++i) { - typeArgs.append("?"); - if (i != ctypeVars - 1) { - typeArgs.append(", "); - } - } - typeArgs.append(">"); - return funcInterfaceName + typeArgs; - } - } - - /** - * Resolves a functional interface type, given the paramter types and the presence/absence of a - * return type. - * - * @param parameters the fully-qualified class names of the parameters - * @param isVoid true iff the method is void - * @param pkgName the package in which a new functional interface should be created, if necessary - * @return the fully-qualified name of a functional interface that is in-scope and is a supertype - * of the given function, according to javac's arity-based typechecking rules for functions - */ - private String resolveFunctionalInterfaceWithFullyQualifiedParameters( - List parameters, boolean isVoid, String pkgName) { - String resolvedWithWildcards = resolveFunctionalInterface(parameters.size(), isVoid, pkgName); - - if (parameters.isEmpty()) { - return resolvedWithWildcards; - } - - String withParameters = - resolvedWithWildcards.substring(0, resolvedWithWildcards.indexOf('<') + 1); - - withParameters += String.join(", ", parameters); - - if (isVoid) { - withParameters += ">"; - return withParameters; - } - - // For method references, ? is sufficient for all return types - withParameters += ", ?>"; - return withParameters; - } - - /** - * Updates the methodRefUsageToSyntheticMethodDef set with the method references in the method - * call's parameters as well as the synthetic definition of the method. - */ - private void updateUnsolvedMethodsWithMethodReferences( - Node methodCall, UnsolvedMethod syntheticMethod) { - Map methodRefs; - if (methodCall instanceof MethodCallExpr) { - MethodCallExpr asMethodCallExpr = (MethodCallExpr) methodCall; - methodRefs = getMethodReferencesFromArguments(asMethodCallExpr.getArguments()); - } else if (methodCall instanceof ObjectCreationExpr) { - ObjectCreationExpr asObjectCreationExpr = (ObjectCreationExpr) methodCall; - methodRefs = getMethodReferencesFromArguments(asObjectCreationExpr.getArguments()); - } else if (methodCall instanceof ExplicitConstructorInvocationStmt) { - ExplicitConstructorInvocationStmt asConstructorCall = - (ExplicitConstructorInvocationStmt) methodCall; - methodRefs = getMethodReferencesFromArguments(asConstructorCall.getArguments()); - } else { - throw new RuntimeException("methodCall is not a method or constructor call"); - } - - for (Map.Entry methodRef : methodRefs.entrySet()) { - // Save the synthetic definition and usage so we can change the method reference - // parameter type later, if needed - Map> unsolvedMethods = - methodRefUsageToSyntheticMethodDef.get(methodRef.getValue().toString()); - if (unsolvedMethods == null) { - unsolvedMethods = new HashMap<>(); - methodRefUsageToSyntheticMethodDef.put(methodRef.getValue().toString(), unsolvedMethods); - } - - Set parameters = unsolvedMethods.get(syntheticMethod); - if (parameters == null) { - parameters = new HashSet<>(); - unsolvedMethods.put(syntheticMethod, parameters); - } - parameters.add(methodRef.getKey()); - } - } - - /** - * Given a class name, this method returns the corresponding package name. - * - * @param className the name of a class, optionally with type arguments. - * @return the package of that class. - */ - public String getPackageFromClassName(String className) { - if (className.contains("<")) { - className = className.substring(0, className.indexOf("<")); - } - if (JavaLangUtils.isJavaLangName(className)) { - // it's important not to accidentally put java.lang classes - // (like e.g., Exception or Throwable) into a wildcard import - // or the current package. - return "java.lang"; - } - String pkg = classAndPackageMap.get(className); - if (pkg != null) { - return pkg; - } else { - // Check if there is a wildcard import. If there isn't always use - // currentPackage. - if (wildcardImports.isEmpty()) { - return currentPackage; - } - // If there is a wildcard import, check if there is a matching class - // in the original codebase in the current package. If so, use that. - if (classfileIsInOriginalCodebase(currentPackage + "." + className)) { - return currentPackage; - } - // If not, then check for each wildcard import if the original codebase - // contains an appropriate class. If so, use it. - for (String wildcardPkg : wildcardImports) { - if (classfileIsInOriginalCodebase(wildcardPkg + "." + className)) { - return wildcardPkg; - } - } - // If none do, then default to the first wildcard import that is not a JDK package. - // TODO: log a warning about this once we have a logger - for (String wildcardPkg : wildcardImports) { - if (!JavaLangUtils.inJdkPackage(wildcardPkg)) { - return wildcardPkg; - } - } - // If we're here, all wildcard imports are jdk imports; use current package instead - return currentPackage; - } - } - - /** - * As the name suggests, this method takes a MethodCallExpr instance as the input and checks if - * the method in that expression is called by an unsolved symbol. - * - * @param method the method call to be analyzed - * @return true if the method involved is called by an unsolved symbol - */ - public static boolean calledByAnUnsolvedSymbol(MethodCallExpr method) { - Optional caller = method.getScope(); - if (!caller.isPresent()) { - return false; - } - Expression callerExpression = caller.get(); - return !canBeSolved(callerExpression); - } - - /** - * This methods check if an Expression instance can be solved by SymbolSolver of JavaParser. If - * the Expression instance can be solved, there is no need to create any synthetic method or class - * for it. - * - * @param expr the expression to be checked - * @return true if the expression can be solved - */ - public static boolean canBeSolved(Expression expr) { - - // The method calculateResolvedType() gets lazy and lacks precision when it comes to handling - // ObjectCreationExpr instances, thus requiring separate treatment for ObjectCreationExpr. - - // Note: JavaParser's lazy approach to ObjectCreationExpr when it comes to - // calculateResolvedType() is reasonable, as the return type typically corresponds to the class - // itself. Consequently, JavaParser does not actively search for constructor declarations within - // the class. While this approach suffices for compilable input, it is inadequate for handling - // incomplete synthetic classes, such as in our case. - if (expr instanceof ObjectCreationExpr) { - try { - expr.asObjectCreationExpr().resolve(); - return true; - } catch (UnsolvedSymbolException | UnsupportedOperationException e) { - return false; - } - } - try { - ResolvedType resolvedType = expr.calculateResolvedType(); - if (resolvedType.isTypeVariable()) { - for (ResolvedTypeParameterDeclaration.Bound bound : - resolvedType.asTypeParameter().getBounds()) { - bound.getType().asReferenceType(); - } - } - return true; - } catch (Exception e) { - return false; - } - } - - /** - * This method takes a MethodCallExpr as an instance, and check if the method involved is called - * by an incomplete class. An incomplete class could either be an original class with unsolved - * symbols or a synthetic class that need to be updated. - * - * @param method a MethodCallExpr instance - * @return true if the method involved is called by an incomplete class - */ - public static boolean calledByAnIncompleteClass(MethodCallExpr method) { - if (calledByAnUnsolvedSymbol(method)) { - return false; - } - if (method.getScope().isEmpty()) { - return false; - } - try { - // use an additional getReturnType() will check the solvability of the method more - // comprehensively. We need to do this because if the return type isn't explicitly shown - // when the method is called, the method might be mistakenly perceived as solved even if the - // return type remains unsolved. - method.resolve().getReturnType(); - return false; - } catch (UnsolvedSymbolException | UnsupportedOperationException e) { - if (e instanceof UnsolvedSymbolException) { - return true; - } - // UnsupportedOperationException is for when the types could not be solved at all, such as - // var or wildcard types. - return false; - } - } - - /** - * Given a MethodCallExpr instance, this method will return the incomplete class for the method - * involved. Thus, make sure that the input method actually belongs to an incomplete class before - * calling this method {@link UnsolvedSymbolVisitor#calledByAnIncompleteClass(MethodCallExpr)} - * (MethodCallExpr)}}. An incomplete class is either an original class with unsolved symbols or a - * synthetic class that needs to be updated. - * - * @param method the method call to be analyzed - * @return the name of the synthetic class of that method - */ - public @FullyQualifiedName String getIncompleteClass(MethodCallExpr method) { - // if calledByAnIncompleteClass returns true for this method call, we know that it has - // a caller. - ResolvedType callerExpression = method.getScope().get().calculateResolvedType(); - if (callerExpression instanceof ResolvedReferenceType) { - ResolvedReferenceType referCaller = (ResolvedReferenceType) callerExpression; - @FullyQualifiedName String callerName = referCaller.getQualifiedName(); - return callerName; - } else if (callerExpression instanceof ResolvedLambdaConstraintType) { - // an example of ConstraintType is the type of "e" in this expression: myMap.map(e -> - // e.toString()) - @FullyQualifiedName String boundedQualifiedType = - callerExpression.asConstraintType().getBound().asReferenceType().getQualifiedName(); - return boundedQualifiedType; - } else if (callerExpression instanceof ResolvedTypeVariable) { - String typeSimpleName = callerExpression.asTypeVariable().describe(); - for (Map> typeScope : typeVariables) { - if (typeScope.containsKey(typeSimpleName)) { - // a type parameter can extend a class and many interfaces. However, the class will always - // be listed first. - return JavaParserUtil.classOrInterfaceTypeToResolvedReferenceType( - typeScope.get(typeSimpleName).get(0)) - .getQualifiedName(); - } - } - } - throw new RuntimeException("Unexpected expression: " + callerExpression); - } - - /** - * This method converts a @FullyQualifiedName classname to a @ClassGetSimpleName classname. Note - * that there is warning suppression here. It is safe to claim that if we split - * a @FullyQualifiedName name by dot ([.]), then the last part is the @ClassGetSimpleName part. - * - * @param fullyQualifiedName a @FullyQualifiedName classname - * @return the @ClassGetSimpleName version of that class - */ - public static @ClassGetSimpleName String fullyQualifiedToSimple( - @FullyQualifiedName String fullyQualifiedName) { - @SuppressWarnings("signature") - @ClassGetSimpleName String simpleName = fullyQualifiedName.substring(fullyQualifiedName.lastIndexOf(".") + 1); - return simpleName; - } - - /** - * Given the name of an unsolved method, this method will return the name of the synthetic return - * type for that method. The name is in @ClassGetSimpleName form - * - * @param methodName the name of a method - * @return that name in @ClassGetSimpleName form - */ - public static @ClassGetSimpleName String returnNameForMethod(String methodName) { - String capitalizedMethodName = toCapital(methodName); - @SuppressWarnings("signature") - @ClassGetSimpleName String returnName = capitalizedMethodName + "ReturnType"; - return returnName; - } - - /** - * This method is to update the missingClass list. The reason we have this update is to add a - * method to an existing class. - * - * @param missedClass the class to be updated - */ - public void updateMissingClass(UnsolvedClassOrInterface missedClass) { - String qualifiedName = missedClass.getQualifiedClassName(); - // If an original class from the input codebase is used with unsolved type parameters, it may be - // misunderstood as an unresolved class. - if (classfileIsInOriginalCodebase(qualifiedName)) { - return; - } - if (JavaLangUtils.inJdkPackage(qualifiedName)) { - return; - } - - // If the input contains something simple like Map.Entry, - // try to avoid creating a synthetic class Entry in package Map if there is - // also a synthetic class for a Map elsewhere (make it an inner class instead). - // There are two possibilities for how this might be encoded: - // 1. the "qualified name" might be "Map.Entry", or - // 2. the class' "simple name" might have a dot in it. For example, the package - // name might be "java.util" and the class name might be "Map.Entry". - String outerClassName = null, innerClassName = null; - // First case, looking for "Map.Entry" pattern - if (JavaParserUtil.isCapital(qualifiedName) - && - // This test checks that it has only one . - qualifiedName.indexOf('.') == qualifiedName.lastIndexOf('.')) { - outerClassName = qualifiedName.substring(0, qualifiedName.indexOf('.')); - innerClassName = qualifiedName.substring(qualifiedName.indexOf('.') + 1); - } - // Second case, looking for "org.example.Map.Entry"-style - String simpleName = missedClass.getClassName(); - if (simpleName.indexOf('.') != -1) { - outerClassName = simpleName.substring(0, simpleName.indexOf('.')); - innerClassName = simpleName.substring(simpleName.indexOf('.') + 1); - } - - if (innerClassName != null && outerClassName != null) { - for (UnsolvedClassOrInterface e : missingClass) { - if (e.getClassName().equals(outerClassName)) { - UnsolvedClassOrInterface innerClass = - new UnsolvedClassOrInterface.UnsolvedInnerClass(innerClassName, e.getPackageName()); - updateMissingClassHelper(missedClass, innerClass); - e.addInnerClass(innerClass); - return; - } - } - // The outer class doesn't exist yet. Create it. - UnsolvedClassOrInterface outerClass = - new UnsolvedClassOrInterface( - outerClassName, missedClass.getPackageName(), false, missedClass.isAnInterface()); - UnsolvedClassOrInterface innerClass = - new UnsolvedClassOrInterface.UnsolvedInnerClass( - innerClassName, missedClass.getPackageName()); - updateMissingClassHelper(missedClass, innerClass); - outerClass.addInnerClass(innerClass); - missingClass.add(outerClass); - return; - } - - for (UnsolvedClassOrInterface e : missingClass) { - if (e.equals(missedClass)) { - updateMissingClassHelper(missedClass, e); - return; - } - } - missingClass.add(missedClass); - } - - /** - * This helper method updates the missing class to with anything in from. The missing classes must - * both represent the same class semantically for it to be sensible to call this method. - * - * @param from the class or interface to use as a source - * @param to the class or interface to be updated - */ - private void updateMissingClassHelper( - UnsolvedClassOrInterface from, UnsolvedClassOrInterface to) { - // add new methods - for (UnsolvedMethod method : from.getMethods()) { - // No need to check for containment, since the methods are stored - // as a set (which does not permit duplicates). - to.addMethod(method); - } - - // add new fields - for (String variablesDescription : from.getClassFields()) { - to.addFields(variablesDescription); - } - if (from.getNumberOfTypeVariables() > 0) { - to.setNumberOfTypeVariables(from.getNumberOfTypeVariables()); - } - - // if a "class" is found to be an interface even once (because it appears - // in an implements clause), then it must be an interface and not a class. - if (from.isAnInterface()) { - to.setIsAnInterfaceToTrue(); - } - } - - /** - * Given the qualified name of a class, this method determines if the corresponding class file - * exists in the original input codebase. - * - * @param qualifiedName the qualified name of a class. - * @return true if the corresponding class file is originally in the input codebase. - */ - public boolean classfileIsInOriginalCodebase(String qualifiedName) { - return this.existingClassesToFilePath.containsKey(qualifiedName); - } - - /** - * The method to update synthetic files. After each run, we might have new synthetic files to be - * created, or new methods to be added to existing synthetic classes. This method will delete all - * the synthetic files from the previous run and re-create those files with the input from the - * current run. - */ - public void updateSyntheticSourceCode() { - for (UnsolvedClassOrInterface missedClass : missingClass) { - this.deleteOldSyntheticClass(missedClass); - this.createMissingClass(missedClass); - } - } - - /** - * This method is to delete a synthetic class. If that synthetic class is not created yet, the - * method will do nothing. - * - * @param missedClass a synthetic class to be deleted - */ - public void deleteOldSyntheticClass(UnsolvedClassOrInterface missedClass) { - String classPackage = missedClass.getPackageName(); - String classDirectory = classPackage.replace(".", "/"); - String filePathStr = - this.rootDirectory + classDirectory + "/" + missedClass.getClassName() + ".java"; - Path filePath = Path.of(filePathStr); - try { - Files.delete(filePath); - } catch (IOException e) { - // It means that the class that has not been created in the previous run of this visitor - } - } - - /** - * This method create a synthetic file for a class that is not in the source codes. The class will - * be created in the root directory of the input. All these synthetic files will be deleted when - * Specimin finishes its run. - * - * @param missedClass the class to be added - */ - public void createMissingClass(UnsolvedClassOrInterface missedClass) { - StringBuilder fileContent = new StringBuilder(); - fileContent.append(missedClass); - String classPackage = missedClass.getPackageName(); - String classDirectory = classPackage.replace(".", "/"); - String filePathStr = - this.rootDirectory + classDirectory + "/" + missedClass.getClassName() + ".java"; - Path filePath = Paths.get(filePathStr); - createdClass.add(filePath); - try { - Path parentPath = filePath.getParent(); - if (parentPath != null && !Files.exists(parentPath)) { - Files.createDirectories(parentPath); - } - try (BufferedWriter writer = - new BufferedWriter(new FileWriter(filePath.toFile(), StandardCharsets.UTF_8))) { - writer.write(fileContent.toString()); - } catch (Exception e) { - throw new Error(e.getMessage()); - } - } catch (IOException e) { - System.out.println("Error creating Java file: " + e.getMessage()); - } - } - - /** - * This method capitalizes a string. For example, "hello" will become "Hello". - * - * @param string the string to be capitalized - * @return the capitalized version of the string - */ - public static String toCapital(String string) { - return Ascii.toUpperCase(string.substring(0, 1)) + string.substring(1); - } - - /** - * Given the name of a class in the @FullyQualifiedName, this method will create a synthetic class - * for that class - * - * @param fullyName the fully-qualified name of the class - * @return the corresponding instance of UnsolvedClass - */ - public static UnsolvedClassOrInterface getSimpleSyntheticClassFromFullyQualifiedName( - @FullyQualifiedName String fullyName) { - if (!JavaParserUtil.isAClassPath(fullyName)) { - throw new RuntimeException( - "Check with JavaParserUtil.isAClassPath first before using" - + " getSimpleSyntheticClassFromFullyQualifiedName. Non-classpath-like name: " - + fullyName); - } - String className = fullyQualifiedToSimple(fullyName); - String packageName = - fullyName.contains("." + className) ? fullyName.replace("." + className, "") : ""; - return new UnsolvedClassOrInterface(className, packageName); - } - - /** - * Checks whether a method call, invoked by a simple class name, is unsolved. - * - * @param method the method call to be examined - * @return true if the method is unsolved and called by a simple class name, otherwise false - */ - public boolean unsolvedAndCalledByASimpleClassName(MethodCallExpr method) { - try { - method.resolve(); - return false; - } catch (Exception e) { - Optional callerExpression = method.getScope(); - if (callerExpression.isEmpty()) { - return false; - } - String callerExpressionString = callerExpression.get().toString(); - return classAndPackageMap.containsKey(callerExpressionString) - || looksLikeSimpleClassName(callerExpressionString); - } - } - - /** - * Checks if the given string looks like it could be a simple class name. This is a coarse - * approximation that should be avoided if possible, because it relies on Java convention instead - * of semantics. - * - * @param name the purported name - * @return true if it looks like a simple class name: it starts with an uppercase letter, has no - * dots, and is not all uppercase - */ - private boolean looksLikeSimpleClassName(String name) { - // this check is not very comprehensive, since a class can be in lowercase, and a method or - // field can be in uppercase. But since this is without the jar paths, this is the best we can - // do. - return Character.isUpperCase(name.charAt(0)) - && name.indexOf('.') == -1 - && !name.toUpperCase(Locale.getDefault()).equals(name); - } - - /** - * Check whether a field, invoked by a simple class name, is unsolved - * - * @param field the field to be checked - * @return true if the field is unsolved and invoked by a simple class name - */ - public boolean unsolvedFieldCalledByASimpleClassName(FieldAccessExpr field) { - try { - field.resolve(); - return false; - } catch (UnsolvedSymbolException | UnsupportedOperationException e) { - String scopeAsString = field.getScope().toString(); - return classAndPackageMap.containsKey(scopeAsString) - || looksLikeSimpleClassName(scopeAsString); - } - } - - /** - * Returns the fully-qualified class name version of a method call invoked by a simple class name. - * - * @param method the method call invoked by a simple class name - * @return the String representation of the method call with a fully-qualified class name - */ - public String toFullyQualifiedCall(MethodCallExpr method) { - if (!unsolvedAndCalledByASimpleClassName(method)) { - throw new RuntimeException( - "Before running convertSimpleCallToFullyQualifiedCall, check if the method call is called" - + " by a simple class name with calledByASimpleClassName"); - } - String methodCall = method.toString(); - String classCaller = method.getScope().get().toString(); - String packageOfClass = getPackageFromClassName(classCaller); - return packageOfClass + "." + methodCall; - } - - /** - * This method checks if a method call is static method that is called by a qualified class name. - * For example, for this call org.package.Class.methodFirst().methodSecond(), this method will - * return true for "org.package.Class.methodFirst()", but not for - * "org.package.Class.methodFirst().methodSecond()". - * - * @param method the method call to be checked - * @return true if the method call is not simple and unsolved - */ - public boolean isAnUnsolvedStaticMethodCalledByAQualifiedClassName(MethodCallExpr method) { - try { - method.resolve().getReturnType(); - return false; - } catch (Exception e) { - Optional callerExpression = method.getScope(); - if (callerExpression.isEmpty()) { - return false; - } - String callerToString = callerExpression.get().toString(); - return JavaParserUtil.isAClassPath(callerToString); - } - } - - /** - * This method checks if a field access expression is a qualified field signature. For example, - * for this field access expression: org.package.Class.firstField.secondField, this method will - * return true for "org.package.Class.firstField", but not for - * "org.package.Class.firstField.secondField". - * - * @param field the field access expression to be checked - * @return true if field is a qualified field signature - */ - public boolean isAQualifiedFieldSignature(String field) { - String caller = field.substring(0, field.lastIndexOf(".")); - return JavaParserUtil.isAClassPath(caller); - } - - /** - * Creates a synthetic class corresponding to a static method call. - * - * @param methodCall the method call to be used as input, in string form. This _must_ be a - * fully-qualified static method call, or this method will throw. - * @param methodArgTypes the types of the arguments of the method, as strings - */ - public void updateClassSetWithQualifiedStaticMethodCall( - String methodCall, List methodArgTypes) { - List methodParts = methodParts(methodCall); - updateClassSetWithQualifiedStaticMethodCallImpl(methodParts, methodArgTypes); - } - - /** - * Breaks apart a static, fully-qualified method call into its dot-separated parts. Helper method - * for {@link #updateClassSetWithQualifiedStaticMethodCallImpl(List, List)}; should not be called - * directly. - * - * @param methodCall a fully-qualified static method call - * @return the list of the parts of the method call - */ - private List methodParts(String methodCall) { - String methodCallWithoutParen = methodCall.substring(0, methodCall.indexOf('(')); - List methodParts = Splitter.onPattern("[.]").splitToList(methodCallWithoutParen); - int lengthMethodParts = methodParts.size(); - if (lengthMethodParts <= 2) { - throw new RuntimeException( - "Need to check the method call with unsolvedAndNotSimple before using" - + " isAnUnsolvedStaticMethodCalledByAQualifiedClassName"); - } - return methodParts; - } - - /** - * Creates a synthetic class corresponding to a static method call. - * - * @param method the method call to be used as input - */ - public void updateClassSetWithStaticMethodCall(MethodCallExpr method) { - String methodCall = method.toString(); - if (!isAnUnsolvedStaticMethodCalledByAQualifiedClassName(method)) { - methodCall = toFullyQualifiedCall(method); - } - List methodParts = methodParts(methodCall); - StringBuilder packageName = new StringBuilder(methodParts.get(0)); - int i = 1; - while (Character.isLowerCase(methodParts.get(i).charAt(0))) { - packageName.append(".").append(methodParts.get(i)); - i++; - } - List methodArguments = getArgumentTypesFromMethodCall(method, packageName.toString()); - UnsolvedMethod createdMethod = - updateClassSetWithQualifiedStaticMethodCallImpl(methodParts, methodArguments); - - if (createdMethod != null) { - updateUnsolvedMethodsWithMethodReferences(method, createdMethod); - } - } - - /** - * Helper method for {@link #updateClassSetWithQualifiedStaticMethodCall(String, List)} and {@link - * #updateClassSetWithStaticMethodCall(MethodCallExpr)}. You should always call one of those - * instead. - * - * @param methodParts the parts of the method call - * @param methodArgTypes the types of the arguments of the method call - * @return the created synthetic method or null if none was created - */ - private @Nullable UnsolvedMethod updateClassSetWithQualifiedStaticMethodCallImpl( - List methodParts, List methodArgTypes) { - // As this code involves complex string operations, we'll use a method call as an example, - // following its progression through the code. - // Suppose this is our method call: com.example.MyClass.process() - // At this point, our method call become: com.example.MyClass.process - int lengthMethodParts = methodParts.size(); - StringBuilder returnTypeClassName = new StringBuilder(toCapital(methodParts.get(0))); - StringBuilder packageName = new StringBuilder(methodParts.get(0)); - // According to the above example, methodName will be process - String methodName = methodParts.get(lengthMethodParts - 1); - @SuppressWarnings( - "signature") // this className is from the second-to-last part of a fully-qualified method - // call, which is the simple name of a class. In this case, it is MyClass. - @ClassGetSimpleName String className = methodParts.get(lengthMethodParts - 2); - // After this loop: returnTypeClassName will be ComExample, and packageName will be com.example - for (int i = 1; i < lengthMethodParts - 2; i++) { - returnTypeClassName.append(toCapital(methodParts.get(i))); - packageName.append(".").append(methodParts.get(i)); - } - - // Before we proceed to making a synthetic class, check if the source class - // is in the original codebase. If so, just add it as a target file instead of - // proceeding to try to make a synthetic class. - String qualifiedName = packageName + "." + className; - if (classfileIsInOriginalCodebase(qualifiedName)) { - addedTargetFiles.add(qualifiedNameToFilePath(qualifiedName)); - gotException(); - return null; - } - - // At this point, returnTypeClassName will be ComExampleMyClassProcessReturnType - returnTypeClassName - .append(toCapital(className)) - .append(toCapital(methodName)) - .append("ReturnType"); - // since returnTypeClassName is just a single long string without any dot in the middle, it will - // be a simple name. - @SuppressWarnings("signature") - @ClassGetSimpleName String thisReturnType = returnTypeClassName.toString(); - UnsolvedClassOrInterface returnClass = - new UnsolvedClassOrInterface(thisReturnType, packageName.toString()); - UnsolvedMethod newMethod = new UnsolvedMethod(methodName, thisReturnType, methodArgTypes); - UnsolvedClassOrInterface classThatContainMethod = - new UnsolvedClassOrInterface(className, packageName.toString()); - newMethod.setStatic(); - classThatContainMethod.addMethod(newMethod); - syntheticMethodReturnTypeAndClass.put(thisReturnType, classThatContainMethod); - this.updateMissingClass(returnClass); - this.updateMissingClass(classThatContainMethod); - - return newMethod; - } - - /** - * Creates a synthetic class corresponding to a static field called by a qualified class name. - * Ensure to check with {@link #isAQualifiedFieldSignature(String)} before calling this method. - * - * @param fieldExpr the field access expression to be used as input. This field access expression - * must be in the form of a qualified class name - * @param isStatic check whether the field is static - * @param isFinal check whether the field is final - */ - public void updateClassSetWithQualifiedFieldSignature( - String fieldExpr, boolean isStatic, boolean isFinal) { - // As this code involves complex string operations, we'll use a field access expression as an - // example, following its progression through the code. - // Suppose this is our field access expression: com.example.MyClass.myField - List fieldParts = Splitter.onPattern("[.]").splitToList(fieldExpr); - int numOfFieldParts = fieldParts.size(); - if (numOfFieldParts <= 2) { - throw new RuntimeException( - "Need to check this field access expression with" - + " isAnUnsolvedStaticFieldCalledByAQualifiedClassName before using this method"); - } - // this is the synthetic type of the field - StringBuilder fieldTypeClassName = new StringBuilder(toCapital(fieldParts.get(0))); - StringBuilder packageName = new StringBuilder(fieldParts.get(0)); - // According to the above example, fieldName will be myField - String fieldName = fieldParts.get(numOfFieldParts - 1); - @SuppressWarnings( - "signature") // this className is from the second-to-last part of a fully-qualified field - // signature, which is the simple name of a class. In this case, it is MyClass. - @ClassGetSimpleName String className = fieldParts.get(numOfFieldParts - 2); - // After this loop: fieldTypeClassName will be ComExample, and packageName will be com.example - for (int i = 1; i < numOfFieldParts - 2; i++) { - fieldTypeClassName.append(toCapital(fieldParts.get(i))); - packageName.append(".").append(fieldParts.get(i)); - } - // At this point, fieldTypeClassName will be ComExampleMyClassMyFieldType - fieldTypeClassName - .append(toCapital(className)) - .append(toCapital(fieldName)) - .append("SyntheticType"); - // since fieldTypeClassName is just a single long string without any dot in the middle, it will - // be a simple name. - @SuppressWarnings("signature") - @ClassGetSimpleName String thisFieldType = fieldTypeClassName.toString(); - UnsolvedClassOrInterface typeClass = - new UnsolvedClassOrInterface(thisFieldType, packageName.toString()); - UnsolvedClassOrInterface classThatContainField = - new UnsolvedClassOrInterface(className, packageName.toString()); - // at this point, fieldDeclaration will become "ComExampleMyClassMyFieldType myField" - String fieldDeclaration = fieldTypeClassName + " " + fieldName; - if (isFinal) { - fieldDeclaration = "final " + fieldDeclaration; - } - if (isStatic) { - // fieldDeclaration will become "static ComExampleMyClassMyFieldType myField = null;" - fieldDeclaration = "static " + fieldDeclaration; - } - fieldDeclaration = - setInitialValueForVariableDeclaration(fieldTypeClassName.toString(), fieldDeclaration); - classThatContainField.addFields(fieldDeclaration); - classAndPackageMap.put(thisFieldType, packageName.toString()); - classAndPackageMap.put(className, packageName.toString()); - syntheticTypeAndClass.put(thisFieldType, classThatContainField); - this.updateMissingClass(typeClass); - this.updateMissingClass(classThatContainField); - } - - /** - * Based on the Map returned by JavaTypeCorrect, this method updates the types of methods in - * synthetic classes. - * - * @param typeToCorrect the Map to be analyzed - * @return true if at least one synthetic type is updated - */ - public boolean updateTypes(Map typeToCorrect) { - boolean atLeastOneTypeIsUpdated = false; - for (String incorrectType : typeToCorrect.keySet()) { - // update incorrectType if it is the type of a field in a synthetic class - if (syntheticTypeAndClass.containsKey(incorrectType)) { - UnsolvedClassOrInterface relatedClass = syntheticTypeAndClass.get(incorrectType); - atLeastOneTypeIsUpdated |= - updateTypeForSyntheticClasses( - relatedClass.getClassName(), - relatedClass.getPackageName(), - true, - incorrectType, - typeToCorrect.get(incorrectType)); - continue; - } - UnsolvedClassOrInterface relatedClass = syntheticMethodReturnTypeAndClass.get(incorrectType); - if (relatedClass != null) { - atLeastOneTypeIsUpdated |= - updateTypeForSyntheticClasses( - relatedClass.getClassName(), - relatedClass.getPackageName(), - false, - incorrectType, - typeToCorrect.get(incorrectType)); - } - // if the above condition is not met, then this incorrectType is a synthetic type for the - // fields of the parent class rather than the return type of some methods - else { - for (UnsolvedClassOrInterface unsolClass : missingClass) { - for (String parentClass : classAndItsParent.values()) { - // TODO: should this also check that unsolClass's package name is - // the correct one for the parent? Martin isn't sure how to do that here. - if (unsolClass.getClassName().equals(parentClass)) { - atLeastOneTypeIsUpdated |= - unsolClass.updateFieldByType(incorrectType, typeToCorrect.get(incorrectType)); - this.deleteOldSyntheticClass(unsolClass); - this.createMissingClass(unsolClass); - } - } - } - } - } - return atLeastOneTypeIsUpdated; - } - - /** - * Based on the map returned by JavaTypeCorrect, this method corrects the extends/implements - * clauses for synthetic classes as needed. - * - * @param typesToExtend the set of synthetic types that need to be updated - * @return true if at least one synthetic type is updated. - */ - public boolean updateTypesWithExtends(Map typesToExtend) { - boolean atLeastOneTypeIsUpdated = false; - Set modifiedClasses = new HashSet<>(); - - for (String typeToExtend : typesToExtend.keySet()) { - String extendedType = typesToExtend.get(typeToExtend); - - // Special case for types with the form Class. In this case, the incompatibility - // is in the type variable X (which is probably a synthetic class whose .class literal - // is used by the target), not in Class itself. - if (JavaLangUtils.bothAreJavaLangClass(typeToExtend, extendedType)) { - // Remove the Class<> - typeToExtend = - typeToExtend.substring(typeToExtend.indexOf('<') + 1, typeToExtend.length() - 1); - extendedType = - extendedType.substring(extendedType.indexOf('<') + 1, extendedType.length() - 1); - } - if (extendedType.startsWith("? extends ")) { - extendedType = extendedType.substring(10); - } - - Iterator iterator = missingClass.iterator(); - while (iterator.hasNext()) { - UnsolvedClassOrInterface missedClass = iterator.next(); - // typeToExtend can be either a simple name or an FQN, due to the limitations - // of Javac - String missedClassBefore = missedClass.toString(); - boolean success = missedClass.extend(typeToExtend, extendedType, this); - if (success) { - iterator.remove(); - modifiedClasses.add(missedClass); - this.deleteOldSyntheticClass(missedClass); - this.createMissingClass(missedClass); - // Only count an update if the text of the synthetic class changes, to avoid infinite - // loops. - atLeastOneTypeIsUpdated |= !missedClassBefore.equals(missedClass.toString()); - } - } - } - - missingClass.addAll(modifiedClasses); - return atLeastOneTypeIsUpdated; - } - - /** - * Updates the types for fields or methods in a synthetic class. - * - * @param className The name of the synthetic class. - * @param packageName The package of the synthetic class. - * @param updateAField True if updating the type of a field, false to update the type of a method. - * @param incorrectTypeName The name of the current incorrect type. - * @param correctTypeName The name of the desired correct type. - * @return true if the update is successful. - */ - public boolean updateTypeForSyntheticClasses( - String className, - String packageName, - boolean updateAField, - String incorrectTypeName, - String correctTypeName) { - // Make sure that correctTypeName is fully qualified, so that we don't need to - // add an import to the synthetic class. - if (!correctTypeName.contains(JavaTypeCorrect.SYNTHETIC_UNCONSTRAINED_TYPE)) { - correctTypeName = lookupFQNs(correctTypeName); - } - boolean updatedSuccessfully = false; - UnsolvedClassOrInterface classToSearch = new UnsolvedClassOrInterface(className, packageName); - Iterator iterator = missingClass.iterator(); - while (iterator.hasNext()) { - UnsolvedClassOrInterface missedClass = iterator.next(); - // Class comparison is based on class name and package name only. - if (missedClass.equals(classToSearch)) { - iterator.remove(); // Remove the outdated version of this synthetic class from the list - if (updateAField) { - updatedSuccessfully |= missedClass.updateFieldByType(incorrectTypeName, correctTypeName); - } else { - updatedSuccessfully |= - missedClass.updateMethodByReturnType(incorrectTypeName, correctTypeName); - } - missingClass.add(missedClass); // Add the modified missedClass back to the list - this.deleteOldSyntheticClass(missedClass); - this.createMissingClass(missedClass); - // incorrectTypeName has to be synthetic, so it will be in the same package as the use - String fullyQualifiedIncorrectTypeName = - missedClass.getPackageName() + "." + incorrectTypeName; - this.migrateType(fullyQualifiedIncorrectTypeName, correctTypeName); - return updatedSuccessfully; - } - } - throw new RuntimeException("Could not find the corresponding missing class!"); - } - - /** - * Corrects synthetic method arguments in which a method call contained a method reference. - * - * @param methodReferencesToCorrect a Map containing the method reference usage (Bar::method) to - * the correct parameter types - * @return true if any method argument was updated - */ - public boolean updateMethodReferenceParameters(Map methodReferencesToCorrect) { - boolean updated = false; - for (String methodReference : methodReferencesToCorrect.keySet()) { - if (methodRefUsageToSyntheticMethodDef.containsKey(methodReference)) { - Map> unsolvedMethodsToArguments = - methodRefUsageToSyntheticMethodDef.get(methodReference); - for (UnsolvedMethod method : unsolvedMethodsToArguments.keySet()) { - Set argumentsToFix = unsolvedMethodsToArguments.get(method); - - String parametersAsString = methodReferencesToCorrect.get(methodReference); - - if (parametersAsString == null) { - throw new RuntimeException("Expected corrected parameter types from JavaTypeCorrect"); - } - - List parameters = new ArrayList<>(); - - for (String parameter : - JavaParserUtil.getReferenceTypesFromCommaSeparatedString(parametersAsString)) { - parameters.add(lookupFQNs(parameter.trim())); - } - - // 1st phase: Assume non-void until javac says it is void - String fixed = - resolveFunctionalInterfaceWithFullyQualifiedParameters( - parameters, false, currentPackage); - - for (Integer argument : argumentsToFix) { - method.correctParameterType(argument.intValue(), fixed); - updated = true; - } - } - } - } - - return updated; - } - - /** - * Corrects synthetic method voidness in which a method call contained a method reference. - * - * @param methodReferencesToCorrect a Map containing the method reference usage (Bar::method) to - * the correct voidness (true if void, false if not) - * @return true if any method was updated - */ - public boolean updateMethodReferenceVoidness(Map methodReferencesToCorrect) { - boolean updated = false; - for (String methodReference : methodReferencesToCorrect.keySet()) { - if (methodRefUsageToSyntheticMethodDef.containsKey(methodReference)) { - Map> unsolvedMethodsToArguments = - methodRefUsageToSyntheticMethodDef.get(methodReference); - for (Map.Entry> method : - unsolvedMethodsToArguments.entrySet()) { - Set argumentsToFix = method.getValue(); - - for (Integer argument : argumentsToFix) { - // 2nd phase: Since we've already corrected the parameter types, now - // we should keep them and update voidness - String arg = method.getKey().getParameterList().get(argument.intValue()); - - String parametersAsString = arg.substring(arg.indexOf('<') + 1, arg.lastIndexOf('>')); - List parameters = - JavaParserUtil.getReferenceTypesFromCommaSeparatedString(parametersAsString); - // Remove the last element; in updateMethodReferenceParameters we assumed that it was - // non-void - parameters.remove(parameters.size() - 1); - - String fixed = - resolveFunctionalInterfaceWithFullyQualifiedParameters( - parameters, methodReferencesToCorrect.get(methodReference), currentPackage); - - method.getKey().correctParameterType(argument.intValue(), fixed); - - updated = true; - } - } - } - } - - return updated; - } - - /** - * Lookup the fully-qualified names of each type in the given string, and replace the simple type - * names in the given string with their fully-qualified equivalents. Return the result. - * - * @param javacType a type from javac - * @return that same type, with simple names in the class-to-package map replaced with FQNs - */ - private String lookupFQNs(String javacType) { - // It's possible that the type could start with a new (synthetic) type variable's declaration. - // That won't be parseable as a type, so strip it first and then re-add it; it isn't parseable - // as a type because, technically, it isn't. However, we post-process what we get from javac - // to add synthetic type variable declarations to some return types (where they'll be placed - // in front of the method). So, for example, we might have something like - // SyntheticTypeVar as the input to this method; the first part is the declaration of the type - // variable, and the second part is a use of the type variable. (There's a test that shows - // this - LambdaBodyStaticUnsolved2Test.) - String typeVarDecl, rest; - if (javacType.startsWith("<")) { - // + 1 to the index to also include the " " that will trail it - typeVarDecl = javacType.substring(0, javacType.indexOf('>') + 1); - rest = javacType.substring(javacType.indexOf('>') + 2); - } else { - typeVarDecl = ""; - rest = javacType; - } - - // need to also remove annotations before parsing, because they aren't in the right - // format. E.g., the string might look like this: - // WeakReference<@org.checkerframework.checker.nullness.qual.Nullable,@org.checkerframework.checker.interning.qual.Interned String []> - int indexOfAt = rest.indexOf('@'); - while (indexOfAt != -1) { - // need to find the next comma or space, disregarding - // any that don't occur when parens are balanced - int idx = indexOfAt; - int endIdx = -1; - int parens = 0; - while (endIdx == -1) { - idx += 1; - char atIdx = rest.charAt(idx); - switch (atIdx) { - case '(': - parens++; - break; - case ')': - parens--; - break; - case ',': - case ' ': - if (parens == 0) { - endIdx = idx; - } - break; - default: - // do nothing - } - } - rest = rest.substring(0, indexOfAt) + rest.substring(endIdx + 1); - indexOfAt = rest.indexOf('@'); - } - - Visitable parsedJavac = StaticJavaParser.parseType(rest); - parsedJavac = - parsedJavac.accept( - new ModifierVisitor() { - @Override - public Visitable visit(ClassOrInterfaceType type, Void p) { - StringBuilder fullyQualifiedName = new StringBuilder(); - if (classAndPackageMap.containsKey(JavaParserUtil.erase(type.asString()))) { - fullyQualifiedName.append( - classAndPackageMap.get(JavaParserUtil.erase(type.asString()))); - fullyQualifiedName.append("."); - } else if (!type.getTypeArguments().isPresent()) { - // This type is in the same package and doesn't contain any type parameters - return super.visit(type, p); - } - - NodeList typeArguments = type.getTypeArguments().orElse(null); - - if (typeArguments != null) { - fullyQualifiedName.append( - type.asString().substring(0, type.asString().indexOf('<') + 1)); - - for (int i = 0; i < typeArguments.size(); i++) { - Type typeArgument = typeArguments.get(i); - lookupTypeArgumentFQN(fullyQualifiedName, typeArgument); - - if (i < typeArguments.size() - 1) { - fullyQualifiedName.append(", "); - } - } - fullyQualifiedName.append(">"); - } else { - fullyQualifiedName.append(type.asString()); - } - - return StaticJavaParser.parseClassOrInterfaceType(fullyQualifiedName.toString()); - } - }, - null); - return typeVarDecl + parsedJavac.toString(); - } - - /** - * Helper method for lookupFQNs which adds the fully qualified type argument to - * fullyQualifiedName. - * - * @param fullyQualifiedName the fully qualified name to build - * @param typeArgument the type argument to lookup - */ - private void lookupTypeArgumentFQN(StringBuilder fullyQualifiedName, Type typeArgument) { - String erased = JavaParserUtil.erase(typeArgument.asString()); - if (classAndPackageMap.containsKey(erased)) { - fullyQualifiedName - .append(classAndPackageMap.get(erased)) - .append(".") - .append(typeArgument.asString()); - } else if (JavaLangUtils.isJavaLangName(erased)) { - // Keep java.lang type arguments as is (Integer, String, etc.) - fullyQualifiedName.append(typeArgument.asString()); - } else if (typeArgument.isWildcardType()) { - WildcardType asWildcardType = typeArgument.asWildcardType(); - - if (asWildcardType.getSuperType().isPresent()) { - fullyQualifiedName.append("? super "); - lookupTypeArgumentFQN(fullyQualifiedName, asWildcardType.getSuperType().get()); - } else if (asWildcardType.getExtendedType().isPresent()) { - fullyQualifiedName.append("? extends "); - lookupTypeArgumentFQN(fullyQualifiedName, asWildcardType.getExtendedType().get()); - } - } else if (JavaParserUtil.isAClassPath(erased)) { - // If it's already a fully qualified name, don't do anything - fullyQualifiedName.append(typeArgument.asString()); - } else { - // If it's not imported, it's probably in the same package - // TODO: handle already fully qualified generic type arguments. Right now JavaTypeCorrect - // only outputs the simple class name, so there is no way to get its fully qualified name - // here without ambiguity (i.e. org.example.Foo and com.example.Foo have different signatures) - // with the same simple class name - // Check MethodReturnFullyQualifiedGenericTest for more details; the current return type in - // Bar.java is com.example.InOtherPackage2 rather than com.foo.InOtherPackage2 (expected) - fullyQualifiedName.append(currentPackage).append(".").append(typeArgument.toString()); - } - } - - /** - * If and only if synthetic classes exist for both input type names, this method migrates all the - * content of the sythetic class for the incorrect type name to the synthetic class for the - * correct type name. This avoid losing information gained about the incorrect type when we - * correct types, which can otherwise lead to non-compilable outputs. - * - * @param incorrectTypeName the fully-qualified incorrect synthetic type - * @param correctTypeName the fully-qualified correct name of the type - */ - private void migrateType(String incorrectTypeName, String correctTypeName) { - // This one may or may not be present. If it is not, exit early and do nothing. - UnsolvedClassOrInterface correctType = getMissingClassWithQualifiedName(correctTypeName); - if (correctType == null) { - if (correctTypeName.contains(JavaTypeCorrect.SYNTHETIC_UNCONSTRAINED_TYPE)) { - // Special case: if the new type name is the synthetic unconstrained type name - // placeholder, there is one more thing to do: replace any and all parameter types - // in synthetic classes that use this (soon to be deleted) synthetic type name - // with java.lang.Object. - for (UnsolvedClassOrInterface unsolvedClass : missingClass) { - for (UnsolvedMethod m : unsolvedClass.getMethods()) { - m.replaceParamWithObject(incorrectTypeName); - } - } - } - return; - } - - // This one is guaranteed to be present. - UnsolvedClassOrInterface incorrectType = getMissingClassWithQualifiedName(incorrectTypeName); - if (incorrectType == null) { - throw new RuntimeException("could not find a synthetic class matching " + incorrectTypeName); - } - - updateMissingClassHelper(incorrectType, correctType); - } - - /** - * Finds the missing/unsolved class with the given fully-qualified name, if one exists. If there - * isn't one, returns null. - * - * @param fqn a fully-qualified name - * @return the unsolved class with that name, or null - */ - private @Nullable UnsolvedClassOrInterface getMissingClassWithQualifiedName(String fqn) { - for (UnsolvedClassOrInterface candidate : missingClass) { - if (candidate.getQualifiedClassName().equals(fqn)) { - return candidate; - } - } - return null; - } - - /** - * Given the name of a class, this method will based on the map classAndItsParent to find the name - * of the super class of the input class - * - * @param className the name of the class to be taken as the input - * @return the name of the super class of the input class - */ - public @ClassGetSimpleName String getParentClass(String className) { - if (classAndItsParent.containsKey(className)) { - return classAndItsParent.get(className); - } else { - throw new RuntimeException("Unfound parent for this class: " + className); - } - } - - /** - * This indirection is here to make it easier to debug infinite loops. Never set gotException - * directly, but instead call this function. - */ - public void gotException() { - if (DEBUG) { - StackTraceElement[] stackTraceElements = Thread.currentThread().getStackTrace(); - System.out.println("setting gotException to true from: " + stackTraceElements[2]); - } - this.gotException = true; - } -} diff --git a/src/main/java/org/checkerframework/specimin/UnsolvedSymbolVisitorProgress.java b/src/main/java/org/checkerframework/specimin/UnsolvedSymbolVisitorProgress.java deleted file mode 100644 index 5930e863e..000000000 --- a/src/main/java/org/checkerframework/specimin/UnsolvedSymbolVisitorProgress.java +++ /dev/null @@ -1,66 +0,0 @@ -package org.checkerframework.specimin; - -import java.util.Objects; -import java.util.Set; -import java.util.StringJoiner; -import org.checkerframework.checker.nullness.qual.Nullable; - -/** A simple class to keep track of the progress of UnsolvedSymbolVisitor. */ -public class UnsolvedSymbolVisitorProgress { - - /** - * Fields and methods that could be called inside the target methods. We call them potential-used - * because the usage check is simply based on the simple names of those members. - */ - private Set potentialUsedMembers; - - /** New files that should be added to the list of target files for the next iteration. */ - private Set addedTargetFiles; - - /** - * A set containing synthetic versions of used classes that are not present in the source code. - * These synthetic versions are created by the UnsolvedSymbolVisitor. - */ - private Set createdSyntheticClass; - - /** - * Constructs a new instance of UnsolvedSymbolVisitorProgress. - * - * @param potentialUsedMembers A set of potential-used members. - * @param addedTargetFiles A set of new files to be added as target files. - * @param createdSyntheticClass A set of synthetic classes created. - */ - public UnsolvedSymbolVisitorProgress( - Set potentialUsedMembers, - Set addedTargetFiles, - Set createdSyntheticClass) { - this.potentialUsedMembers = potentialUsedMembers; - this.addedTargetFiles = addedTargetFiles; - this.createdSyntheticClass = createdSyntheticClass; - } - - @Override - public boolean equals(@Nullable Object obj) { - if (!(obj instanceof UnsolvedSymbolVisitorProgress)) { - return false; - } - UnsolvedSymbolVisitorProgress other = (UnsolvedSymbolVisitorProgress) obj; - return potentialUsedMembers.equals(other.potentialUsedMembers) - && addedTargetFiles.equals(other.addedTargetFiles) - && createdSyntheticClass.equals(other.createdSyntheticClass); - } - - @Override - public int hashCode() { - return Objects.hash(potentialUsedMembers, addedTargetFiles, createdSyntheticClass); - } - - @Override - public String toString() { - return new StringJoiner(",", "[", "]") - .add("potentialUsedMembers=" + potentialUsedMembers.toString()) - .add("addedTargetFiles=" + addedTargetFiles.toString()) - .add("createdSyntheticClass=" + createdSyntheticClass.toString()) - .toString(); - } -} diff --git a/src/main/java/org/checkerframework/specimin/UnusedImportRemoverVisitor.java b/src/main/java/org/checkerframework/specimin/UnusedImportRemoverVisitor.java deleted file mode 100644 index db9c34d4b..000000000 --- a/src/main/java/org/checkerframework/specimin/UnusedImportRemoverVisitor.java +++ /dev/null @@ -1,312 +0,0 @@ -package org.checkerframework.specimin; - -import com.github.javaparser.ast.ImportDeclaration; -import com.github.javaparser.ast.Node; -import com.github.javaparser.ast.PackageDeclaration; -import com.github.javaparser.ast.expr.AnnotationExpr; -import com.github.javaparser.ast.expr.Expression; -import com.github.javaparser.ast.expr.FieldAccessExpr; -import com.github.javaparser.ast.expr.MarkerAnnotationExpr; -import com.github.javaparser.ast.expr.MethodCallExpr; -import com.github.javaparser.ast.expr.NameExpr; -import com.github.javaparser.ast.expr.NormalAnnotationExpr; -import com.github.javaparser.ast.expr.SingleMemberAnnotationExpr; -import com.github.javaparser.ast.type.ClassOrInterfaceType; -import com.github.javaparser.ast.visitor.ModifierVisitor; -import com.github.javaparser.ast.visitor.Visitable; -import com.github.javaparser.resolution.UnsolvedSymbolException; -import com.github.javaparser.resolution.declarations.ResolvedEnumConstantDeclaration; -import com.github.javaparser.resolution.declarations.ResolvedFieldDeclaration; -import com.github.javaparser.resolution.declarations.ResolvedMethodDeclaration; -import com.github.javaparser.resolution.declarations.ResolvedValueDeclaration; -import com.google.common.base.Splitter; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; - -/** - * Removes all unused import statements from a compilation unit. This visitor should be used after - * pruning. - */ -public class UnusedImportRemoverVisitor extends ModifierVisitor { - /** - * A Map of fully qualified type/member names, or a wildcard import, to the actual import - * declaration itself - */ - private final Map typeNamesToImports = new HashMap<>(); - - /** A map of fully qualified imports to their simple import names */ - private final Map fullyQualifiedImportsToSimple = new HashMap<>(); - - /** A set of all fully qualified type/member names in the current compilation unit */ - private final Set usedImports = new HashSet<>(); - - /** A set of unsolvable member names. */ - private final Set unsolvedMembers = new HashSet<>(); - - /** The package of the current compilation unit. */ - private String currentPackage = ""; - - /** - * Removes unused imports from the current compilation unit and resets the state to be used with - * another compilation unit. - */ - public void removeUnusedImports() { - for (Map.Entry entry : typeNamesToImports.entrySet()) { - if (!usedImports.contains(entry.getKey())) { - // In special cases (namely with MethodCallExprs containing lambdas), JavaParser can have - // trouble resolving it, so we should preserve its imports through approximation by simple - // method names. - if (!unsolvedMembers.isEmpty()) { - String simpleName = fullyQualifiedImportsToSimple.get(entry.getKey()); - - if (simpleName != null && unsolvedMembers.contains(simpleName)) { - continue; - } - } - entry.getValue().remove(); - } else if (!currentPackage.equals("") - && entry.getKey().startsWith(currentPackage + ".") - && !entry.getKey().substring(currentPackage.length() + 1).contains(".")) { - // If importing a class from the same package, remove the unnecessary import - entry.getValue().remove(); - } - } - - typeNamesToImports.clear(); - usedImports.clear(); - fullyQualifiedImportsToSimple.clear(); - unsolvedMembers.clear(); - currentPackage = ""; - } - - @Override - public Node visit(ImportDeclaration decl, Void arg) { - String importName = decl.getNameAsString(); - - // ImportDeclaration does not contain the asterisk by default; we need to add it - if (decl.isAsterisk()) { - importName += ".*"; - } else { - String className = importName.substring(0, importName.lastIndexOf(".")); - String elementName = importName.replace(className + ".", ""); - fullyQualifiedImportsToSimple.put(importName, elementName); - } - - typeNamesToImports.put(importName, decl); - return super.visit(decl, arg); - } - - @Override - public Visitable visit(PackageDeclaration node, Void arg) { - currentPackage = node.getNameAsString(); - - return super.visit(node, arg); - } - - @Override - public Visitable visit(ClassOrInterfaceType type, Void arg) { - String typeAsString = type.getName().asString(); - // Workaround for a JavaParser bug: see UnsolvedSymbolVisitor#visit(ClassOrInterfaceType) - if (!JavaParserUtil.isCapital(typeAsString)) { - return super.visit(type, arg); - } - // Also, if it's already fully qualified, it's not tied to an import - if (JavaParserUtil.isAClassPath(typeAsString)) { - List elements = Splitter.onPattern("\\.").splitToList(typeAsString); - // Heuristic for FQNs: the second-to-last element is a package name, and so starts with - // a lower-case letter. This is important to avoid returning too early when encountering - // e.g., "Map.Entry". - if (!JavaParserUtil.isCapital(elements.get(elements.size() - 2))) { - return super.visit(type, arg); - } - } - - String fullyQualified; - try { - fullyQualified = JavaParserUtil.erase(type.resolve().describe()); - } catch (UnsolvedSymbolException ex) { - // Specimin made an error somewhere if this type is unresolvable; - // TODO: fix this once MethodReturnFullyQualifiedGenericTest is fixed - return super.visit(type, arg); - } - - if (!fullyQualified.contains(".")) { - // Type variable; definitely not imported - return super.visit(type, arg); - } - - // Check must include both the fully qualified name and the wildcard to match a potential import - // e.g. java.util.List and java.util.*. Moreover, we need to do this check recursively for all - // the classes in the name until we encounter a package: for example, - // if the code uses "Map.Entry" and imports "java.util.Map", we need to preserve that import. - // We do this heuristically here by assuming class names start with a capital and package names - // do not. - String lastElement = fullyQualified.substring(fullyQualified.lastIndexOf('.') + 1); - while (JavaParserUtil.isCapital(lastElement)) { - String withoutLast = fullyQualified.substring(0, fullyQualified.lastIndexOf('.')); - String wildcard = withoutLast + ".*"; - usedImports.add(fullyQualified); - usedImports.add(wildcard); - fullyQualified = withoutLast; - lastElement = fullyQualified.substring(fullyQualified.lastIndexOf('.') + 1); - } - return super.visit(type, arg); - } - - @Override - public Visitable visit(FieldAccessExpr expr, Void arg) { - if (expr.hasScope()) { - handleScopeExpression(expr.getScope()); - } - return super.visit(expr, arg); - } - - @Override - public Visitable visit(NameExpr expr, Void arg) { - if (expr.getParentNode().isPresent()) { - // If it's a field access/method call expression, other methods will handle this - if (expr.getParentNode().get() instanceof FieldAccessExpr) { - return super.visit(expr, arg); - } - if (expr.getParentNode().get() instanceof MethodCallExpr) { - // visit(MethodCallExpr) only handles it if it's the scope - MethodCallExpr parent = (MethodCallExpr) expr.getParentNode().get(); - if (parent.hasScope() && parent.getScope().get().toString().equals(expr.toString())) { - return super.visit(expr, arg); - } - } - } - - ResolvedValueDeclaration resolved = expr.resolve(); - - // Handle statically imported fields - // e.g. - // import static java.lang.Math.PI; - // double x = PI; - // ^^ - if (resolved.isField()) { - ResolvedFieldDeclaration asField = resolved.asField(); - String declaringType = JavaParserUtil.erase(asField.declaringType().getQualifiedName()); - // Check for both cases, e.g.: java.lang.Math.PI and java.lang.Math.* - usedImports.add(declaringType + "." + asField.getName()); - usedImports.add(declaringType + ".*"); - } else if (resolved.isEnumConstant()) { - ResolvedEnumConstantDeclaration asEnumConstant = resolved.asEnumConstant(); - String declaringType = JavaParserUtil.erase(asEnumConstant.getType().describe()); - // Importing the enum itself, a static import of a specific enum value, or a wildcard - // for all values of the enum - // e.g. - // com.example.Enum, com.example.Enum.VALUE, com.example.Enum.* - usedImports.add(declaringType); - usedImports.add(declaringType + "." + asEnumConstant.getName()); - usedImports.add(declaringType + ".*"); - } - return super.visit(expr, arg); - } - - @Override - public Visitable visit(MethodCallExpr expr, Void arg) { - ResolvedMethodDeclaration resolved; - try { - resolved = expr.resolve(); - } catch (UnsupportedOperationException ex) { - // Lambdas can raise an UnsupportedOperationException - return super.visit(expr, arg); - } catch (UnsolvedSymbolException | IllegalStateException ex) { - unsolvedMembers.add(expr.getNameAsString()); - return super.visit(expr, arg); - } - - if (resolved.isStatic()) { - // If it has a scope, the parent class is imported - if (expr.hasScope()) { - handleScopeExpression(expr.getScope().get()); - } else { - // Handle statically imported methods - // e.g. - // import static java.lang.Math.sqrt; - // sqrt(1); - // Check for qualified name and the wildcard, e.g.: java.lang.Math.sqrt and java.lang.Math.* - usedImports.add(JavaParserUtil.erase(resolved.getQualifiedName())); - usedImports.add(JavaParserUtil.erase(resolved.declaringType().getQualifiedName()) + ".*"); - } - } - - return super.visit(expr, arg); - } - - @Override - public Visitable visit(MarkerAnnotationExpr anno, Void arg) { - handleAnnotation(anno); - - return super.visit(anno, arg); - } - - @Override - public Visitable visit(NormalAnnotationExpr anno, Void arg) { - handleAnnotation(anno); - - return super.visit(anno, arg); - } - - @Override - public Visitable visit(SingleMemberAnnotationExpr anno, Void arg) { - handleAnnotation(anno); - - return super.visit(anno, arg); - } - - /** - * Helper method to handle the scope type in a FieldAccessExpr or MethodCallExpr. - * - * @param scope The scope as an Expression - */ - private void handleScopeExpression(Expression scope) { - // Workaround for a JavaParser bug: see UnsolvedSymbolVisitor#visit(ClassOrInterfaceType) - if (!JavaParserUtil.isCapital(scope.toString())) { - return; - } - - String fullyQualified = JavaParserUtil.erase(scope.calculateResolvedType().describe()); - - if (!fullyQualified.contains(".")) { - // If there is no ., it is not a class (e.g. this.values.length) - return; - } - - String wildcard = getWildcardFromClassOrMemberName(fullyQualified); - - usedImports.add(fullyQualified); - usedImports.add(wildcard); - } - - /** - * Helper method to resolve all annotation expressions and add them to usedImports. - * - * @param anno The annotation expression to handle - */ - private void handleAnnotation(AnnotationExpr anno) { - String fullyQualified = JavaParserUtil.erase(anno.resolve().getQualifiedName()); - String wildcard = getWildcardFromClassOrMemberName(fullyQualified); - - // Check for the fully qualified class name, or a wildcard import of the annotation's package - // e.g. java.lang.annotation.Target and java.lang.annotation.* - usedImports.add(fullyQualified); - usedImports.add(wildcard); - } - - /** - * Helper method to convert a fully qualified class/member name into a wildcard e.g. {@code - * java.lang.Math.sqrt} --> {@code java.lang.Math.*} - * - * @param fullyQualified The fully qualified name - * @return {@code fullyQualified} with the text after the last dot replaced with an asterisk - * ({@code *}) - */ - private static String getWildcardFromClassOrMemberName(String fullyQualified) { - return fullyQualified.substring(0, fullyQualified.lastIndexOf('.')) + ".*"; - } -} diff --git a/src/main/java/org/checkerframework/specimin/modularity/ModularityModel.java b/src/main/java/org/checkerframework/specimin/modularity/ModularityModel.java index 29cd125df..81ef218c2 100644 --- a/src/main/java/org/checkerframework/specimin/modularity/ModularityModel.java +++ b/src/main/java/org/checkerframework/specimin/modularity/ModularityModel.java @@ -1,5 +1,7 @@ package org.checkerframework.specimin.modularity; +import com.google.common.base.Ascii; + /** * This interface represents the differences between modularity models. A single instance of a class * that implements this one represents a particular modularity model for an analysis. @@ -19,16 +21,13 @@ public interface ModularityModel { * @return the corresponding modularity model */ public static ModularityModel createModularityModel(String modularityModel) { - switch (modularityModel) { - case "cf": - case "javac": - return new CheckerFrameworkModularityModel(); - case "nullaway": - return new NullAwayModularityModel(); - default: - throw new RuntimeException( - "Unsupported modularity model. Options are: \"cf\", \"javac\", \"nullaway\""); - } + return switch (Ascii.toLowerCase(modularityModel)) { + case "cf", "javac" -> new CheckerFrameworkModularityModel(); + case "nullaway" -> new NullAwayModularityModel(); + default -> + throw new RuntimeException( + "Unsupported modularity model. Options are: \"cf\", \"javac\", \"nullaway\""); + }; } /** diff --git a/src/main/java/org/checkerframework/specimin/unsolved/FullyQualifiedNameGenerator.java b/src/main/java/org/checkerframework/specimin/unsolved/FullyQualifiedNameGenerator.java new file mode 100644 index 000000000..0e70a21bf --- /dev/null +++ b/src/main/java/org/checkerframework/specimin/unsolved/FullyQualifiedNameGenerator.java @@ -0,0 +1,2045 @@ +package org.checkerframework.specimin.unsolved; + +import com.github.javaparser.ast.CompilationUnit; +import com.github.javaparser.ast.ImportDeclaration; +import com.github.javaparser.ast.Node; +import com.github.javaparser.ast.NodeList; +import com.github.javaparser.ast.PackageDeclaration; +import com.github.javaparser.ast.body.BodyDeclaration; +import com.github.javaparser.ast.body.CallableDeclaration; +import com.github.javaparser.ast.body.MethodDeclaration; +import com.github.javaparser.ast.body.Parameter; +import com.github.javaparser.ast.body.TypeDeclaration; +import com.github.javaparser.ast.body.VariableDeclarator; +import com.github.javaparser.ast.expr.AnnotationExpr; +import com.github.javaparser.ast.expr.AssignExpr; +import com.github.javaparser.ast.expr.BinaryExpr; +import com.github.javaparser.ast.expr.BinaryExpr.Operator; +import com.github.javaparser.ast.expr.ConditionalExpr; +import com.github.javaparser.ast.expr.Expression; +import com.github.javaparser.ast.expr.FieldAccessExpr; +import com.github.javaparser.ast.expr.LambdaExpr; +import com.github.javaparser.ast.expr.MethodCallExpr; +import com.github.javaparser.ast.expr.MethodReferenceExpr; +import com.github.javaparser.ast.expr.Name; +import com.github.javaparser.ast.expr.NameExpr; +import com.github.javaparser.ast.nodeTypes.NodeWithArguments; +import com.github.javaparser.ast.nodeTypes.NodeWithCondition; +import com.github.javaparser.ast.nodeTypes.NodeWithExtends; +import com.github.javaparser.ast.nodeTypes.NodeWithImplements; +import com.github.javaparser.ast.nodeTypes.NodeWithSimpleName; +import com.github.javaparser.ast.nodeTypes.NodeWithTraversableScope; +import com.github.javaparser.ast.nodeTypes.NodeWithType; +import com.github.javaparser.ast.nodeTypes.NodeWithVariables; +import com.github.javaparser.ast.stmt.ForEachStmt; +import com.github.javaparser.ast.stmt.ReturnStmt; +import com.github.javaparser.ast.type.ClassOrInterfaceType; +import com.github.javaparser.ast.type.ReferenceType; +import com.github.javaparser.ast.type.Type; +import com.github.javaparser.ast.type.TypeParameter; +import com.github.javaparser.ast.type.UnionType; +import com.github.javaparser.resolution.Resolvable; +import com.github.javaparser.resolution.UnsolvedSymbolException; +import com.github.javaparser.resolution.declarations.AssociableToAST; +import com.github.javaparser.resolution.declarations.ResolvedMethodDeclaration; +import com.github.javaparser.resolution.declarations.ResolvedMethodLikeDeclaration; +import com.github.javaparser.resolution.declarations.ResolvedReferenceTypeDeclaration; +import com.github.javaparser.resolution.declarations.ResolvedTypeParameterDeclaration; +import com.github.javaparser.resolution.declarations.ResolvedValueDeclaration; +import com.github.javaparser.resolution.types.ResolvedReferenceType; +import com.github.javaparser.resolution.types.ResolvedType; +import com.google.common.base.Ascii; +import com.google.common.base.Splitter; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.checker.signature.qual.ClassGetSimpleName; +import org.checkerframework.specimin.JavaLangUtils; +import org.checkerframework.specimin.JavaParserUtil; + +/** + * Helper class for {@link UnsolvedSymbolGenerator}. Generates all FQNs based on an expression or + * type. + */ +public class FullyQualifiedNameGenerator { + /** Constant prefix for generated synthetic types. */ + private static final String SYNTHETIC_TYPE_FOR = "SyntheticTypeFor"; + + /** Constant suffix for generated return type symbols. */ + private static final String RETURN_TYPE = "ReturnType"; + + /** Map of fully qualified names to their corresponding compilation units. */ + private final Map fqnToCompilationUnits; + + /** Map of fully qualified names to their generated symbol alternates. */ + private final Map> generatedSymbols; + + /** + * Create a new instance. Needs a map of type FQNs to compilation units for symbol resolution. + * + * @param fqnToCompilationUnits The map of FQNs to compilation units + * @param generatedSymbols The map of FQNs to generated symbols. Should be the same instance used + * in UnsolvedSymbolGenerator. + */ + public FullyQualifiedNameGenerator( + Map fqnToCompilationUnits, + Map> generatedSymbols) { + this.fqnToCompilationUnits = fqnToCompilationUnits; + this.generatedSymbols = generatedSymbols; + } + + /** + * When evaluating an expression, there is only one possible type. However, the location of an + * expression could vary, depending on the parent classes/interfaces of the class which holds the + * expression. This method and {@link #getFQNsForExpressionType(Expression)} return different + * values; for example, for the method call {@code foo()}, this method could return the class name + * from a static import or from unsolved super classes. The latter method would return {@code + * FooReturnType} or its solvable equivalent. + * + *

For example, take expression a.b where a is of type A. A implements interface B, and + * interface B extends many different unsolved interfaces C, D, E, F, etc. + * + *

Thus, a static field b could be in any of the interfaces C, D, E, F, and we need to + * differentiate between these interfaces. + * + *

This method may also return an empty map if the method/field is located in a solvable type. + * + * @param expr The expression to do the analysis upon + * @return A collection of sets of FQNs. Each set represents a different type that the + * expression's declaration could be located in. + */ + public Collection> getFQNsForExpressionLocation(Expression expr) { + Collection> alreadyGenerated = + getFQNsForExpressionLocationIfRepresentsGenerated(expr); + + if (alreadyGenerated != null) { + return alreadyGenerated; + } + + if (expr.isNameExpr() || (expr.isMethodCallExpr() && !expr.hasScope())) { + String name = JavaParserUtil.erase(((NodeWithSimpleName) expr).getNameAsString()); + + CompilationUnit cu = expr.findCompilationUnit().get(); + + ImportDeclaration staticImport = JavaParserUtil.getImportDeclaration(name, cu, true); + + if (staticImport != null) { + String holdingType = staticImport.getName().getQualifier().get().toString(); + return Set.of(Set.of(holdingType)); + } + + // If not static, from parent + Collection> result = + getFQNsOfAllUnresolvableParents(JavaParserUtil.getEnclosingClassLike(expr), expr) + .values(); + + if (result.isEmpty() && expr.isNameExpr() && JavaParserUtil.isAClassName(expr.toString())) { + // All parent classes/interfaces are solvable, and do not contain this field. In this case, + // it's also likely that this could be a type + + result = Set.of(getFQNsFromErasedClassName(expr.toString(), expr.toString(), cu, expr)); + } + + return result; + } + + Expression scope; + if (expr.isFieldAccessExpr()) { + scope = expr.asFieldAccessExpr().getScope(); + } else if (expr.isMethodCallExpr()) { + scope = expr.asMethodCallExpr().getScope().get(); + } else if (expr.isObjectCreationExpr()) { + return Set.of( + getFQNsFromClassOrInterfaceType(expr.asObjectCreationExpr().getType()).erasedFqns()); + } else if (expr.isMethodReferenceExpr()) { + scope = expr.asMethodReferenceExpr().getScope(); + } else { + throw new RuntimeException( + "Unexpected call to getFQNsForExpressionLocation with expression type " + + expr.getClass()); + } + + if (scope.isSuperExpr() || scope.isThisExpr()) { + return getFQNsOfAllUnresolvableParents(JavaParserUtil.getEnclosingClassLike(expr), expr) + .values(); + } + + try { + ResolvedType resolved = scope.calculateResolvedType(); + + if (resolved.isTypeVariable()) { + ResolvedTypeParameterDeclaration typeParam = resolved.asTypeVariable().asTypeParameter(); + + TypeParameter attachedTypeParameter = + (TypeParameter) JavaParserUtil.findAttachedNode(typeParam, fqnToCompilationUnits); + + NodeList bound = attachedTypeParameter.getTypeBound(); + + if (bound == null) { + return Set.of(); + } + + Map> potentialFQNs = new LinkedHashMap<>(); + + for (ClassOrInterfaceType type : bound) { + try { + resolved = type.resolve(); + + Optional optionalTypeDecl = + resolved.asReferenceType().getTypeDeclaration(); + + if (optionalTypeDecl.isPresent() && optionalTypeDecl.get().toAst().isPresent()) { + TypeDeclaration typeDecl = + (TypeDeclaration) + JavaParserUtil.getTypeFromQualifiedName( + optionalTypeDecl.get().getQualifiedName(), fqnToCompilationUnits); + + if (typeDecl == null) { + // We shouldn't ever encounter this error. If toAst() returns a non-empty value, + // then it is in the project + throw new RuntimeException("Cannot be null here"); + } + + Map> toAdd = getFQNsOfAllUnresolvableParents(typeDecl, type); + + for (String key : toAdd.keySet()) { + if (potentialFQNs.containsKey(key)) { + potentialFQNs.get(key).addAll(toAdd.get(key)); + } else { + potentialFQNs.put(key, new LinkedHashSet<>(toAdd.get(key))); + } + } + } + } catch (UnsolvedSymbolException ex) { + // Type not resolvable + } + + String simpleClassName = JavaParserUtil.erase(type.getNameAsString()); + + // Since we're looking at the location of the expression, the type arguments are not + // relevant here. + if (potentialFQNs.containsKey(simpleClassName)) { + potentialFQNs + .get(simpleClassName) + .addAll(getFQNsFromClassOrInterfaceType(type).erasedFqns()); + } else { + potentialFQNs.put(simpleClassName, getFQNsFromClassOrInterfaceType(type).erasedFqns()); + } + } + + return potentialFQNs.values(); + } + } catch (UnsolvedSymbolException ex) { + // Type not resolvable + } + + // Handle union types (NameExpr could be an exception capture in a catch clause) + if (scope.isNameExpr()) { + try { + ResolvedValueDeclaration resolvedValueDeclaration = scope.asNameExpr().resolve(); + + Node toAst = + JavaParserUtil.tryFindAttachedNode(resolvedValueDeclaration, fqnToCompilationUnits); + + if (toAst instanceof Parameter param && param.getType().isUnionType()) { + UnionType unionType = param.getType().asUnionType(); + + Map> result = new LinkedHashMap<>(); + + for (ReferenceType type : unionType.getElements()) { + try { + // If a type in the union type is resolvable, the location of the expression will + // be in a built-in Java superclass. In this case, return an empty map. Follow this + // reasoning: + // If a union type is UnsolvedException | NullPointerException, then any method + // called on the NameExpr + // representing an exception of this type will be in Exception or Throwable (or + // NullPointerException if + // UnsolvedException extended it). + + // TODO: handle a case where a user-defined exception could be solvable but a parent + // class of that exception is not. + type.resolve(); + return Set.of(); + } catch (UnsolvedSymbolException ex) { + // continue + } + + // Safe to just use erased fqns: member location does not depend on what the type + // argument is + Set fqns = getFQNsFromType(type).erasedFqns(); + + if (fqns.isEmpty()) { + continue; + } + + String simple = JavaParserUtil.getSimpleNameFromQualifiedName(fqns.iterator().next()); + result.put(simple, fqns); + } + + return result.values(); + } + } catch (UnsolvedSymbolException ex) { + // Not a union type since declaration is unresolvable + } + } + + // After these cases, we've handled all exceptions where the scope could be various different + // locations. + // Non-super members with scope are located in the same type as the scope; there is only one + // possible type for these scopes. + + Set fqnSets = getFQNsForExpressionType(scope); + + Set> result = new LinkedHashSet<>(); + + for (FullyQualifiedNameSet fqnSet : fqnSets) { + result.add(fqnSet.erasedFqns()); + } + + return result; + } + + /** + * Gets the location of an expression if its scope is a generated symbol. If the scope is not a + * generated symbol (or not yet generated), returns null. + * + * @param expr The method call or field access expression + * @return A collection of FQN sets, each set representing a different type, or null if the scope + * is not a generated symbol + */ + private @Nullable Collection> getFQNsForExpressionLocationIfRepresentsGenerated( + Expression expr) { + String name; + Expression scope; + + if (expr.isFieldAccessExpr()) { + name = expr.asFieldAccessExpr().getNameAsString(); + scope = expr.asFieldAccessExpr().getScope(); + } else if (expr.isMethodCallExpr()) { + name = expr.asMethodCallExpr().getNameAsString(); + scope = expr.asMethodCallExpr().getScope().orElse(null); + } else if (expr.isNameExpr()) { + name = expr.asNameExpr().getNameAsString(); + scope = null; + } else { + return null; + } + + // Static import + if (!expr.hasScope()) { + ImportDeclaration importDecl = + JavaParserUtil.getImportDeclaration(name, expr.findCompilationUnit().get(), true); + + if (importDecl != null) { + String location = importDecl.getName().getQualifier().get().toString(); + + return List.of(Set.of(location)); + } + } else if (scope instanceof MethodCallExpr scopeAsMethodCall) { + // Try an overly-generous approach by using both null and java.lang.Object + Set potentialScopeScopeFQNs = + generateMethodFQNsWithSideEffect( + scopeAsMethodCall, getFQNsForExpressionLocation(scopeAsMethodCall), null, true); + potentialScopeScopeFQNs.addAll( + generateMethodFQNsWithSideEffect( + scopeAsMethodCall, getFQNsForExpressionLocation(scopeAsMethodCall), null, false)); + + UnsolvedMethodAlternates genMethod = + (UnsolvedMethodAlternates) findUnsolvedSymbolIfGenerated(potentialScopeScopeFQNs); + if (genMethod != null) { + return genMethod.getReturnTypes().stream() + .map(returnTypes -> returnTypes.getFullyQualifiedNames()) + .toList(); + } + } + // Handle FieldAccessExpr/NameExpr together here + else if (scope instanceof FieldAccessExpr || scope instanceof NameExpr) { + Set potentialScopeScopeFQNs = new LinkedHashSet<>(); + + String fieldName = ((NodeWithSimpleName) scope).getNameAsString(); + for (Set set : getFQNsForExpressionLocation(scope)) { + for (String potentialScopeFQN : set) { + potentialScopeScopeFQNs.add(potentialScopeFQN + "#" + fieldName); + } + } + + UnsolvedFieldAlternates genField = + (UnsolvedFieldAlternates) findUnsolvedSymbolIfGenerated(potentialScopeScopeFQNs); + if (genField != null) { + return genField.getTypes().stream().map(type -> type.getFullyQualifiedNames()).toList(); + } + } + + return null; + } + + /** + * Given an expression, this method returns possible FQNs of its type. If the type is an array, + * all FQNs will have the same number of array brackets. Most calls to this method will return a + * set of length 1; the only cases so far where that is not the case is when multiple methods + * correspond to the same method reference or if the expression is seen in a BinaryExpr. + * + * @param expr The expression + * @return The potential FQNs of the type of the given expression. + */ + public Set getFQNsForExpressionType(Expression expr) { + return getFQNsForExpressionTypeImpl(expr, true); + } + + /** + * Given an expression, this method returns possible FQNs of its type. This is the implementation + * for {@link #getFQNsForExpressionLocation(Expression)}; use this method instead to prevent + * StackOverflowError when recursing. + * + * @param expr The expression + * @param canRecurse Whether or not this method can call itself + * @return The potential FQNs of the type of the given expression. + */ + private Set getFQNsForExpressionTypeImpl( + Expression expr, boolean canRecurse) { + // If the type of the expression can already be calculated, return it + // Throws UnsupportedOperationException for annotation expressions + // Handle class expressions separately; their type argument may be a private type, which + // is handled below. + if (!expr.isAnnotationExpr() + && !expr.isClassExpr() + && JavaParserUtil.isExprTypeResolvable(expr)) { + ResolvedType type = expr.calculateResolvedType(); + + return Set.of(getFQNsForResolvedType(type)); + } + + Set alreadyGenerated = getExpressionTypesIfRepresentsGenerated(expr); + + if (alreadyGenerated != null) { + return alreadyGenerated; + } + + // super + if (expr.isSuperExpr()) { + return Set.of(getFQNsFromClassOrInterfaceType(JavaParserUtil.getSuperClass(expr))); + } + // scope of a static field/method + else if (JavaParserUtil.isAClassPath(expr.toString())) { + Expression scoped = expr; + + while (scoped instanceof NodeWithTraversableScope + && ((NodeWithTraversableScope) scoped).traverseScope().isPresent()) { + scoped = ((NodeWithTraversableScope) scoped).traverseScope().get(); + } + + return Set.of( + new FullyQualifiedNameSet( + getFQNsFromErasedClassName( + scoped.toString(), expr.toString(), expr.findCompilationUnit().get(), expr))); + } else if (expr.isNameExpr() && JavaParserUtil.isAClassName(expr.toString())) { + return Set.of( + new FullyQualifiedNameSet( + getFQNsFromErasedClassName( + expr.toString(), expr.toString(), expr.findCompilationUnit().get(), expr))); + } + // method ref + else if (expr.isMethodReferenceExpr()) { + return getFQNsForMethodReferenceType(expr.asMethodReferenceExpr()); + } + // lambda + else if (expr.isLambdaExpr()) { + return getFQNsForLambdaType(expr.asLambdaExpr()); + } + // Special wrapper for method reference scopes + else if (expr.isTypeExpr()) { + return Set.of(getFQNsFromType(expr.asTypeExpr().getType())); + } + // cast expression + else if (expr.isCastExpr()) { + return Set.of( + getFQNsFromClassOrInterfaceType(expr.asCastExpr().getType().asClassOrInterfaceType())); + } else if (expr.isClassExpr()) { + return Set.of( + new FullyQualifiedNameSet( + Set.of("java.lang.Class"), + List.of( + new FullyQualifiedNameSet( + getFQNsFromType(expr.asClassExpr().getType()).erasedFqns(), + List.of(), + "? extends")))); + } else if (expr.isAnnotationExpr()) { + return Set.of(getFQNsFromAnnotation(expr.asAnnotationExpr())); + } else if (expr.isArrayAccessExpr()) { + Set result = new LinkedHashSet<>(); + for (FullyQualifiedNameSet fqns : + getFQNsForExpressionType(expr.asArrayAccessExpr().getName())) { + + Set fixedFQNs = new LinkedHashSet<>(); + for (String fqn : fqns.erasedFqns()) { + if (fqn.endsWith("[]")) { + fixedFQNs.add(fqn.substring(0, fqn.length() - 2)); + } + } + result.add(new FullyQualifiedNameSet(fixedFQNs, fqns.typeArguments())); + } + return result; + } else if (expr.isObjectCreationExpr()) { + return Set.of(getFQNsFromClassOrInterfaceType(expr.asObjectCreationExpr().getType())); + } else if (expr.isConditionalExpr()) { + return Stream.concat( + getFQNsForExpressionType(expr.asConditionalExpr().getThenExpr()).stream(), + getFQNsForExpressionType(expr.asConditionalExpr().getElseExpr()).stream()) + .collect(Collectors.toCollection(LinkedHashSet::new)); + } + + // local variable / field / method call / object creation expression / any other case + // where the expression is resolvable BUT the type of the expression may not be. + try { + @Nullable Set solvableDeclarationTypeFQNs = + getFQNsForTypeOfSolvableExpression(expr); + if (solvableDeclarationTypeFQNs != null) { + return solvableDeclarationTypeFQNs; + } + } catch (UnsolvedSymbolException ex) { + @Nullable FullyQualifiedNameSet findableDeclarationTypeFQNs = + getFQNsForExpressionInAnonymousClass(expr); + if (findableDeclarationTypeFQNs != null) { + return Set.of(findableDeclarationTypeFQNs); + } + // Not a local variable or field + } + + // Handle the cases where the type of the expression can be inferred from surrounding context + if (expr.hasParentNode()) { + @Nullable Set fromLHS = + getFQNsFromSurroundingContextType(expr, canRecurse); + if (fromLHS != null) { + return fromLHS; + } + } + + // Handle binary expressions after surrounding context because the binary expression could be on + // the right-hand side where the left side type is known. + if (expr.isBinaryExpr()) { + BinaryExpr binary = expr.asBinaryExpr(); + Operator operator = binary.getOperator(); + + // Boolean + if (operator == Operator.AND + || operator == Operator.OR + || operator == Operator.EQUALS + || operator == Operator.NOT_EQUALS + || operator == Operator.LESS + || operator == Operator.GREATER + || operator == Operator.LESS_EQUALS + || operator == Operator.GREATER_EQUALS) { + return Set.of(new FullyQualifiedNameSet("boolean")); + } else { + // Treat all other cases; type on one side is equal to the other + Set leftType = + getFQNsForExpressionTypeImpl(binary.getLeft(), canRecurse); + Set rightType = + getFQNsForExpressionTypeImpl(binary.getRight(), canRecurse); + + // Remaining operators only work with primitive/boxed/String types + // Safe to call isJavaLangOrPrimitiveName since any non-primitive/boxed/String type would be + // a synthetic type here + if (leftType.size() == 1 + && leftType.iterator().next().erasedFqns().size() == 1 + && JavaLangUtils.isJavaLangOrPrimitiveName( + leftType.iterator().next().erasedFqns().iterator().next())) { + return leftType; + } + if (rightType.size() == 1 + && rightType.iterator().next().erasedFqns().size() == 1 + && JavaLangUtils.isJavaLangOrPrimitiveName( + rightType.iterator().next().erasedFqns().iterator().next())) { + return rightType; + } + + Set result = new LinkedHashSet<>(); + for (String validType : JavaLangUtils.getTypesForOp(operator.asString())) { + if (JavaParserUtil.isAClassName(validType)) { + validType = "java.lang." + validType; + } + result.add(new FullyQualifiedNameSet(validType)); + } + + return result; + } + } + + // field/method located in unsolvable super class, but it's not explicitly marked by + // super. It could also be a static member, either statically imported, a static member + // of an imported class, or a static member of a class in the same package. + String fqnOfStaticMember = JavaParserUtil.getFQNIfStaticMember(expr); + if (fqnOfStaticMember != null) { + return Set.of( + new FullyQualifiedNameSet( + generateFQNForTheTypeOfAStaticallyImportedMember( + fqnOfStaticMember, expr.isMethodCallExpr()))); + } + + if (expr.isNameExpr()) { + String name = expr.asNameExpr().getNameAsString(); + + CompilationUnit cu = expr.findCompilationUnit().get(); + + ImportDeclaration importDecl = JavaParserUtil.getImportDeclaration(name, cu, false); + + if (importDecl != null) { + // The name expr could also be a class: calling this method on the scope of Baz.foo + // where Baz is the name expr could mean that it's an imported type and thus static. + if (!importDecl.isStatic()) { + return Set.of( + new FullyQualifiedNameSet( + getFQNsFromErasedClassName( + expr.toString(), expr.toString(), expr.findCompilationUnit().get(), expr))); + } + return Set.of( + new FullyQualifiedNameSet( + generateFQNForTheTypeOfAStaticallyImportedMember( + importDecl.getNameAsString(), false))); + } + + String exprTypeName = SYNTHETIC_TYPE_FOR + toCapital(name); + return Set.of( + new FullyQualifiedNameSet( + getFQNsFromErasedClassName( + exprTypeName, exprTypeName, expr.findCompilationUnit().get(), null))); + } else if (expr.isFieldAccessExpr()) { + Expression scope = expr.asFieldAccessExpr().getScope(); + + String exprTypeName; + if (scope.isThisExpr() || scope.isSuperExpr()) { + exprTypeName = SYNTHETIC_TYPE_FOR + toCapital(expr.asFieldAccessExpr().getNameAsString()); + } else { + String scopeType = + getFQNsForExpressionType(expr.asFieldAccessExpr().getScope()) + .iterator() + .next() + .erasedFqns() + .iterator() + .next(); + + return Set.of( + new FullyQualifiedNameSet( + generateFQNForTheTypeOfAStaticallyImportedMember( + scopeType + "." + expr.asFieldAccessExpr().getNameAsString(), false))); + } + + // Place in the same package as its scope type + while (scope.hasScope()) { + scope = ((NodeWithTraversableScope) scope).traverseScope().get(); + } + + Set fqns = getFQNsForExpressionType(scope).iterator().next().erasedFqns(); + Set result = new LinkedHashSet<>(); + + for (String fqn : fqns) { + result.add(fqn.substring(0, fqn.lastIndexOf('.') + 1) + exprTypeName); + } + + return Set.of(new FullyQualifiedNameSet(result)); + } else if (expr.isMethodCallExpr()) { + String exprTypeName = toCapital(expr.asMethodCallExpr().getNameAsString()) + RETURN_TYPE; + // Place in the same package as its scope type + Set fqns = getFQNsForExpressionLocation(expr).iterator().next(); + Set result = new LinkedHashSet<>(); + + for (String fqn : fqns) { + result.add(fqn.substring(0, fqn.lastIndexOf('.') + 1) + exprTypeName); + } + + return Set.of(new FullyQualifiedNameSet(result)); + } + + // Hitting this error means we forgot to account for a case + throw new RuntimeException( + "Unknown expression type: " + expr.getClass() + "; expression value: " + expr); + } + + /** + * Gets the type of a generated symbol if it exists, or else returns null if the expression type + * does not have a generated symbol. + * + * @param expression The expression to check + * @return The set of fully qualified name sets if the expression represents a generated symbol, + * or null otherwise + */ + public @Nullable Set getExpressionTypesIfRepresentsGenerated( + Expression expression) { + if (expression.isMethodCallExpr()) { + MethodCallExpr methodCall = expression.asMethodCallExpr(); + Collection> potentialScopeFQNs = getFQNsForExpressionLocation(methodCall); + // Try an overly-generous approach by using both null and java.lang.Object + Set methodFQNs = + generateMethodFQNsWithSideEffect(methodCall, potentialScopeFQNs, null, true); + methodFQNs.addAll( + generateMethodFQNsWithSideEffect(methodCall, potentialScopeFQNs, null, false)); + + UnsolvedMethodAlternates unsolvedMethodAlternates = + (UnsolvedMethodAlternates) findUnsolvedSymbolIfGenerated(methodFQNs); + + if (unsolvedMethodAlternates != null) { + return unsolvedMethodAlternates.getReturnTypes().stream() + .map(this::convertMemberTypeToFQNSet) + .collect(Collectors.toCollection(LinkedHashSet::new)); + } + + if (!methodFQNs.isEmpty()) { + // For purposes of seeing if the method is part of Throwable/Exception/RuntimeException, + // getting the signature of the first is enough since they should all be the same if their + // declaring + // type extends it + String methodSignature = methodFQNs.iterator().next(); + methodSignature = methodSignature.substring(methodSignature.indexOf('#') + 1); + + for (Set declaringTypeFQNs : potentialScopeFQNs) { + UnsolvedClassOrInterfaceAlternates declaringType = + (UnsolvedClassOrInterfaceAlternates) findUnsolvedSymbolIfGenerated(declaringTypeFQNs); + + if (declaringType == null) { + continue; + } + if (declaringType.doesExtend(SolvedMemberType.JAVA_LANG_EXCEPTION) + || declaringType.doesExtend(SolvedMemberType.JAVA_LANG_ERROR)) { + if (JavaLangUtils.getJavaLangThrowableMethods().containsKey(methodSignature)) { + return Set.of( + new FullyQualifiedNameSet( + JavaLangUtils.getJavaLangThrowableMethods().get(methodSignature))); + } + } + } + } + } else if (expression.isFieldAccessExpr()) { + FieldAccessExpr fieldAccess = expression.asFieldAccessExpr(); + Collection> potentialScopeFQNs = getFQNsForExpressionLocation(fieldAccess); + Set potentialFQNs = new HashSet<>(); + + for (Set scopeFQNs : potentialScopeFQNs) { + for (String fqn : scopeFQNs) { + potentialFQNs.add(fqn + "#" + fieldAccess.getNameAsString()); + } + } + + UnsolvedFieldAlternates unsolvedFieldAlternates = + (UnsolvedFieldAlternates) findUnsolvedSymbolIfGenerated(potentialFQNs); + + if (unsolvedFieldAlternates != null) { + return unsolvedFieldAlternates.getTypes().stream() + .map(this::convertMemberTypeToFQNSet) + .collect(Collectors.toCollection(LinkedHashSet::new)); + } + } else if (expression.isNameExpr()) { + NameExpr nameExpr = expression.asNameExpr(); + Collection> potentialScopeFQNs = getFQNsForExpressionLocation(nameExpr); + Set potentialFQNs = new HashSet<>(); + + for (Set scopeFQNs : potentialScopeFQNs) { + for (String fqn : scopeFQNs) { + potentialFQNs.add(fqn + "#" + nameExpr.getNameAsString()); + } + } + + UnsolvedFieldAlternates unsolvedFieldAlternates = + (UnsolvedFieldAlternates) findUnsolvedSymbolIfGenerated(potentialFQNs); + + if (unsolvedFieldAlternates != null) { + return unsolvedFieldAlternates.getTypes().stream() + .map(this::convertMemberTypeToFQNSet) + .collect(Collectors.toCollection(LinkedHashSet::new)); + } + } else if (expression.isConditionalExpr()) { + ConditionalExpr conditionalExpr = expression.asConditionalExpr(); + Collection> potentialScopeFQNs1 = + getFQNsForExpressionLocation(conditionalExpr.getThenExpr()); + Collection> potentialScopeFQNs2 = + getFQNsForExpressionLocation(conditionalExpr.getElseExpr()); + Set potentialFQNs = new HashSet<>(); + + for (Set scopeFQNs : potentialScopeFQNs1) { + for (String fqn : scopeFQNs) { + potentialFQNs.add(fqn + "#" + conditionalExpr.getCondition().toString()); + } + } + for (Set scopeFQNs : potentialScopeFQNs2) { + for (String fqn : scopeFQNs) { + potentialFQNs.add(fqn + "#" + conditionalExpr.getCondition().toString()); + } + } + + UnsolvedFieldAlternates unsolvedFieldAlternates = + (UnsolvedFieldAlternates) findUnsolvedSymbolIfGenerated(potentialFQNs); + + if (unsolvedFieldAlternates != null) { + return unsolvedFieldAlternates.getTypes().stream() + .map(this::convertMemberTypeToFQNSet) + .collect(Collectors.toCollection(LinkedHashSet::new)); + } + } else if (expression.isMethodReferenceExpr()) { + MethodReferenceExpr methodRef = expression.asMethodReferenceExpr(); + Collection> potentialScopeFQNs = getFQNsForExpressionLocation(methodRef); + + boolean isConstructor = JavaParserUtil.erase(methodRef.getIdentifier()).equals("new"); + String methodName = + isConstructor + ? JavaParserUtil.getSimpleNameFromQualifiedName(methodRef.getScope().toString()) + : JavaParserUtil.erase(methodRef.getIdentifier()); + + Set potentialFullyQualifiedNames = new LinkedHashSet<>(); + + for (Set set : potentialScopeFQNs) { + for (String fqn : set) { + potentialFullyQualifiedNames.add(fqn + "#" + methodName + "("); + } + } + + Set generatedMethods = new LinkedHashSet<>(); + + for (Entry> genSymbolEntry : + generatedSymbols.entrySet()) { + if (!(genSymbolEntry.getValue() instanceof UnsolvedMethodAlternates method)) { + continue; + } + + String fullyQualifiedMethodName = + genSymbolEntry.getKey().substring(0, genSymbolEntry.getKey().indexOf('(') + 1); + + if (potentialFullyQualifiedNames.contains(fullyQualifiedMethodName)) { + generatedMethods.add(method); + } + } + + if (!generatedMethods.isEmpty()) { + Set functionalInterfaces = new LinkedHashSet<>(); + + for (UnsolvedMethodAlternates method : generatedMethods) { + for (UnsolvedMethod alternate : method.getAlternates()) { + List parameterTypes = new ArrayList<>(); + + if (!method.isStatic() && methodRef.getScope().isTypeExpr()) { + parameterTypes.add(getFQNsFromType(methodRef.getScope().asTypeExpr().getType())); + } + + for (MemberType parameter : alternate.getParameterList()) { + FullyQualifiedNameSet converted = convertMemberTypeToFQNSet(parameter); + parameterTypes.add( + new FullyQualifiedNameSet( + converted.erasedFqns(), converted.typeArguments(), "? extends")); + } + + functionalInterfaces.add( + getQualifiedNameOfFunctionalInterface( + parameterTypes, + !isConstructor && alternate.getReturnType().toString().equals("void"))); + } + } + + if (!functionalInterfaces.isEmpty()) { + return functionalInterfaces; + } + } + } + + return null; + } + + /** + * Gets method FQNs given a method call expression and a collection of sets of potential FQNs + * (each set represents a different potential declaring type). argumentToParameterPotentialFQNs is + * passed in and modified as a side effect (argument mapping to potential type FQNs); if this is + * null, then there is no side effect. + * + * @param methodCall The method call expression + * @param potentialScopeFQNs Potential scope FQNs + * @param argumentToParameterPotentialFQNs A map of arguments to their type FQNs; pass in null if + * no side effect is desired. + * @param keepNullInsteadOfObject True if you want to use null instead of Object as part of the + * signature. + * @return The set of strings representing the potential FQNs of this method + */ + public Set generateMethodFQNsWithSideEffect( + MethodCallExpr methodCall, + Collection> potentialScopeFQNs, + @Nullable Map> argumentToParameterPotentialFQNs, + boolean keepNullInsteadOfObject) { + List> simpleNames = new ArrayList<>(); + + for (Expression argument : methodCall.getArguments()) { + if (argument.isNullLiteralExpr() && keepNullInsteadOfObject) { + simpleNames.add(Set.of("null")); + if (argumentToParameterPotentialFQNs != null) { + argumentToParameterPotentialFQNs.put(argument, Set.of(new FullyQualifiedNameSet("null"))); + } + continue; + } + + Set fqns = getFQNsForExpressionType(argument); + + Set simpleNamesOfThisParameterType = new LinkedHashSet<>(); + for (FullyQualifiedNameSet fqnSet : fqns) { + String first = fqnSet.erasedFqns().iterator().next(); + String simpleName = JavaParserUtil.getSimpleNameFromQualifiedName(first); + simpleNamesOfThisParameterType.add(simpleName); + } + + simpleNames.add(simpleNamesOfThisParameterType); + if (argumentToParameterPotentialFQNs != null) { + argumentToParameterPotentialFQNs.put(argument, fqns); + } + } + + Set potentialFQNs = new LinkedHashSet<>(); + + for (List simpleNamesCombo : JavaParserUtil.generateAllCombinations(simpleNames)) { + for (Set set : potentialScopeFQNs) { + for (String potentialScopeFQN : set) { + potentialFQNs.add( + potentialScopeFQN + + "#" + + methodCall.getNameAsString() + + "(" + + String.join(", ", simpleNamesCombo) + + ")"); + } + } + } + + return potentialFQNs; + } + + /** + * Converts MemberType to a FullyQualifiedNameSet. + * + * @param memberType The member type + * @return The FQN set + */ + private FullyQualifiedNameSet convertMemberTypeToFQNSet(MemberType memberType) { + List typeArguments = + memberType.getTypeArguments().stream().map(this::convertMemberTypeToFQNSet).toList(); + + if (memberType instanceof WildcardMemberType wildcard) { + MemberType bound = wildcard.getBound(); + if (bound != null) { + FullyQualifiedNameSet boundFQNs = convertMemberTypeToFQNSet(bound); + boolean isUpperBound = wildcard.isUpperBounded(); + return new FullyQualifiedNameSet( + boundFQNs.erasedFqns(), + boundFQNs.typeArguments(), + isUpperBound ? "? extends" : "? super"); + } else { + return FullyQualifiedNameSet.UNBOUNDED_WILDCARD; + } + } + return new FullyQualifiedNameSet(memberType.getFullyQualifiedNames(), typeArguments); + } + + /** + * Finds an existing unsolved symbol that matches any of the given potential fully qualified + * names. + * + * @param potentialFQNs A set of potential fully qualified names to check against the generated + * symbols. + * @return An existing UnsolvedSymbolAlternates if found; otherwise, null. + */ + private @Nullable UnsolvedSymbolAlternates findUnsolvedSymbolIfGenerated( + Set potentialFQNs) { + UnsolvedSymbolAlternates alreadyGenerated = null; + for (String potentialFQN : potentialFQNs) { + alreadyGenerated = generatedSymbols.get(potentialFQN); + + if (alreadyGenerated != null) { + return alreadyGenerated; + } + } + return null; + } + + /** + * Given a resolved type, return the FQNs of its type. + * + * @param resolvedType The resolved type + * @return The FQNs of the type + */ + public FullyQualifiedNameSet getFQNsForResolvedType(ResolvedType resolvedType) { + if (resolvedType.isReferenceType()) { + String qualifiedName = resolvedType.asReferenceType().getQualifiedName(); + + if (JavaParserUtil.areTypeOrOuterTypesPrivate(qualifiedName, fqnToCompilationUnits)) { + // If private, then we use java.lang.Object since this method is likely for use by + // symbols not in the current class. + qualifiedName = "java.lang.Object"; + } + return new FullyQualifiedNameSet( + Set.of(qualifiedName), + resolvedType.asReferenceType().typeParametersValues().stream() + .map(this::getFQNsForResolvedType) + .toList()); + } else if (resolvedType.isWildcard()) { + if (resolvedType.asWildcard().isBounded()) { + FullyQualifiedNameSet bound = + getFQNsForResolvedType(resolvedType.asWildcard().getBoundedType()); + boolean isUpperBound = resolvedType.asWildcard().isUpperBounded(); + + return new FullyQualifiedNameSet( + bound.erasedFqns(), bound.typeArguments(), isUpperBound ? "? extends" : "? super"); + } else { + return FullyQualifiedNameSet.UNBOUNDED_WILDCARD; + } + } + + if (resolvedType.isNull()) { + return new FullyQualifiedNameSet("null"); + } + + return new FullyQualifiedNameSet(resolvedType.describe()); + } + + /** + * Given a method reference expression, return the FQNs of its functional interface. + * + * @param methodRef The method reference expression + * @return The FQNs of its functional interface + */ + private Set getFQNsForMethodReferenceType(MethodReferenceExpr methodRef) { + // Applicable java.lang.Object methods. Key is method signature, value is return type. + Set> applicableObjectMethods = + JavaLangUtils.getJavaLangObjectMethods().entrySet().stream() + .filter(k -> k.getKey().startsWith(methodRef.getIdentifier())) + .collect(Collectors.toCollection(LinkedHashSet::new)); + + if (!applicableObjectMethods.isEmpty()) { + Set functionalInterfaces = new LinkedHashSet<>(); + for (Entry method : applicableObjectMethods) { + // If the method is applicable, we can use it as a functional interface + String parameters = + method + .getKey() + .substring(method.getKey().indexOf('(') + 1, method.getKey().lastIndexOf(')')); + List paramList = new ArrayList<>(); + + if (methodRef.getScope().isTypeExpr()) { + FullyQualifiedNameSet nonWildcard = + getFQNsFromType(methodRef.getScope().asTypeExpr().getType()); + paramList.add( + new FullyQualifiedNameSet( + nonWildcard.erasedFqns(), nonWildcard.typeArguments(), "? extends")); + } + + for (String param : parameters.split(",\\s*", -1)) { + if (!param.isEmpty()) { + paramList.add(new FullyQualifiedNameSet(param)); + } + } + + functionalInterfaces.add( + getQualifiedNameOfFunctionalInterface( + paramList, method.getValue().equals("void"), methodRef)); + } + return functionalInterfaces; + } + + Set surroundingContextFQNs = + getFQNsFromSurroundingContextType(methodRef, true); + + if (surroundingContextFQNs != null) { + return surroundingContextFQNs; + } + + List declarations = + JavaParserUtil.getMethodDeclarationsFromMethodRef(methodRef); + + if (!declarations.isEmpty()) { + Set functionalInterfaces = new LinkedHashSet<>(); + for (ResolvedMethodLikeDeclaration resolved : declarations) { + functionalInterfaces.add(getFunctionalInterfaceForResolvedMethod(methodRef, resolved)); + } + + return functionalInterfaces; + } + + FullyQualifiedNameSet functionalInterface; + // If we can't find the method declaration, then pretend it has no parameters (and void if a + // method) + if (methodRef.getIdentifier().equals("new")) { + functionalInterface = getQualifiedNameOfFunctionalInterface(0, false, methodRef); + } else { + // If the method ref is unresolvable, use a built-in type (Runnable) + functionalInterface = getQualifiedNameOfFunctionalInterface(0, true, methodRef); + } + + return Set.of(functionalInterface); + } + + /** + * Gets the functional interface for a resolved method, given a method reference. + * + * @param methodRef The method reference expression + * @param resolved The resolved method-like declaration + * @return The fully qualified name set representing the functional interface + */ + public FullyQualifiedNameSet getFunctionalInterfaceForResolvedMethod( + MethodReferenceExpr methodRef, ResolvedMethodLikeDeclaration resolved) { + List parameters = new ArrayList<>(); + + if (methodRef.getScope().isTypeExpr() + && resolved.isMethod() + && resolved.asMethod().isStatic()) { + FullyQualifiedNameSet nonWildcard = + getFQNsFromType(methodRef.getScope().asTypeExpr().getType()); + parameters.add( + new FullyQualifiedNameSet( + nonWildcard.erasedFqns(), nonWildcard.typeArguments(), "? extends")); + } + + Node attached = JavaParserUtil.tryFindAttachedNode(resolved, fqnToCompilationUnits); + if (attached != null) { + CallableDeclaration callableDecl = (CallableDeclaration) attached; + + for (Parameter param : callableDecl.getParameters()) { + FullyQualifiedNameSet nonWildcard = getFQNsFromType(param.getType()); + + if (nonWildcard.erasedFqns().size() == 1 + && nonWildcard.erasedFqns().iterator().next().equals("java.lang.Object")) { + parameters.add(FullyQualifiedNameSet.UNBOUNDED_WILDCARD); + continue; + } + + parameters.add( + new FullyQualifiedNameSet( + nonWildcard.erasedFqns(), nonWildcard.typeArguments(), "? extends")); + } + } else { + // By reflection or jar + for (int i = 0; i < resolved.getNumberOfParams(); i++) { + try { + String fqn = resolved.getParam(i).describeType(); + if (fqn.equals("java.lang.Object")) { + parameters.add(FullyQualifiedNameSet.UNBOUNDED_WILDCARD); + } else { + parameters.add(new FullyQualifiedNameSet(fqn)); + } + } catch (UnsolvedSymbolException ex) { + parameters.add(FullyQualifiedNameSet.UNBOUNDED_WILDCARD); + } + } + } + + // Getting the return type could also cause an unsolved symbol exception, but we only care + // if it's void or not + boolean isVoid; + try { + // Constructors are never void + isVoid = + resolved instanceof ResolvedMethodDeclaration method + ? method.getReturnType().isVoid() + : false; + } catch (UnsolvedSymbolException ex) { + isVoid = false; + } + + return getQualifiedNameOfFunctionalInterface(parameters, isVoid, methodRef); + } + + /** + * Given a lambda expression, return the FQNs of its functional interface. + * + * @param lambda The lambda expression + * @return The FQNs of its functional interface + */ + private Set getFQNsForLambdaType(LambdaExpr lambda) { + boolean isVoid; + + if (lambda.getExpressionBody().isPresent()) { + Expression body = lambda.getExpressionBody().get(); + Set fqns = getFQNsForExpressionType(body).iterator().next().erasedFqns(); + isVoid = fqns.size() == 1 && fqns.iterator().next().equals("void"); + } else { + isVoid = + !lambda.getBody().asBlockStmt().getStatements().stream() + .anyMatch(stmt -> stmt instanceof ReturnStmt); + } + + FullyQualifiedNameSet functionalInterface = + getQualifiedNameOfFunctionalInterface(lambda.getParameters().size(), isVoid, lambda); + + Node body = lambda.getBody(); + List usedNameExprs = body.findAll(NameExpr.class); + + // List index corresponds with the type argument index, and each set represents potential + // types to replace these old type arguments (which were just ?) + List> newTypeArguments = new ArrayList<>(); + + for (int i = 0; i < functionalInterface.typeArguments().size(); i++) { + if (i >= lambda.getParameters().size()) { + // Non-void return types + newTypeArguments.add(Set.of(functionalInterface.typeArguments().get(i))); + continue; + } + + Parameter param = lambda.getParameters().get(i); + // If the param has no type defined, then we need to use a bounded wildcard + // so the output program compiles + if (!param.getType().isUnknownType()) { + newTypeArguments.add(Set.of(functionalInterface.typeArguments().get(i))); + continue; + } + + String paramName = param.getNameAsString(); + NameExpr use = + usedNameExprs.stream() + .filter(nameExpr -> nameExpr.getNameAsString().equals(paramName)) + .findFirst() + .orElse(null); + if (use != null) { + Set useType = getFQNsForExpressionType(use); + newTypeArguments.add( + useType.stream() + .map( + type -> + new FullyQualifiedNameSet( + type.erasedFqns(), type.typeArguments(), "? extends")) + .collect(Collectors.toCollection(LinkedHashSet::new))); + } else { + newTypeArguments.add(Set.of(functionalInterface.typeArguments().get(i))); + } + } + + Set result = new LinkedHashSet<>(); + for (List combination : + JavaParserUtil.generateAllCombinations(newTypeArguments)) { + result.add(new FullyQualifiedNameSet(functionalInterface.erasedFqns(), combination)); + } + + return result; + } + + /** + * Given an expression that can be resolved, return a best shot at its type based on a resolved + * declaration. May return null if a type cannot be found from the resolved declaration. This + * method will also throw an UnsolvedSymbolException if .resolve() fails. + * + * @param expr A resolvable expression + * @return A set of FQNs, or null if unfound + */ + private @Nullable Set getFQNsForTypeOfSolvableExpression(Expression expr) { + if (!(expr instanceof Resolvable)) { + return null; + } + + Node node = null; + Object resolved = null; + + try { + resolved = ((Resolvable) expr).resolve(); + } catch (UnsupportedOperationException ex) { + resolved = + JavaParserUtil.tryFindCorrespondingDeclarationForConstraintQualifiedExpression(expr); + } catch (UnsolvedSymbolException ex) { + if (expr instanceof NodeWithArguments withArguments) { + resolved = + JavaParserUtil.tryFindSingleCallableForNodeWithUnresolvableArguments( + withArguments, fqnToCompilationUnits); + } + + if (resolved == null) { + throw ex; + } + } + + if (resolved instanceof AssociableToAST associableToAST) { + node = JavaParserUtil.tryFindAttachedNode(associableToAST, fqnToCompilationUnits); + } else if (resolved instanceof Node directlyFoundAst) { + node = directlyFoundAst; + } + + // Field declaration and variable declaration expressions + if (node instanceof NodeWithVariables withVariables) { + Type type = withVariables.getElementType(); + Expression initializer = null; + + // See if we can find the exact variable, because ElementType gets rid of arrays + for (VariableDeclarator varDecl : withVariables.getVariables()) { + if (varDecl.getName().equals(((NodeWithSimpleName) expr).getName())) { + type = varDecl.getType(); + initializer = varDecl.getInitializer().orElse(null); + break; + } + } + + if (type.isVarType()) { + if (initializer == null) { + throw new RuntimeException("Cannot have a var type with no initializer"); + } + return getFQNsForExpressionType(initializer); + } + + if (!type.isUnknownType()) { + // Keep going if var/unknown type + return Set.of(getFQNsFromType(type)); + } + } + // methods, new ClassName() + else if (node instanceof NodeWithType withType) { + Type type = withType.getType(); + if (!type.isUnknownType()) { + // Keep going if unknown type: note this is only possible with Parameter + return Set.of(getFQNsFromType(type)); + } + } + + return null; + } + + /** + * Given an expression that is in an anonymous class definition, return its FQNs. This method is + * necessary for fields that are defined within the anonymous class if the parent class is not + * solvable. + * + * @param expr The expression to analyze + * @return A set of FQNs, or null if unfound + */ + private @Nullable FullyQualifiedNameSet getFQNsForExpressionInAnonymousClass(Expression expr) { + Object resolved = JavaParserUtil.tryResolveExpressionIfInAnonymousClass(expr); + + BodyDeclaration decl = null; + if (resolved instanceof AssociableToAST associableToAST) { + // Expressions are linked to NodeWithType + Node node = JavaParserUtil.tryFindAttachedNode(associableToAST, fqnToCompilationUnits); + + if (node instanceof NodeWithType withType) { + return getFQNsFromType(withType.getType()); + } else if (node instanceof BodyDeclaration bodyDecl) { + decl = bodyDecl; + } + } + + if (decl == null) { + // Check if the expression is within the anonymous class. This method is necessary because + // if the parent class of the anonymous class is not solvable, fields/methods defined within + // are also unsolvable + decl = JavaParserUtil.tryFindCorrespondingDeclarationInAnonymousClass(expr); + } + + if (decl == null) { + return null; + } + + if (decl.isFieldDeclaration()) { + for (VariableDeclarator var : decl.asFieldDeclaration().getVariables()) { + if (var.getName().equals(((NodeWithSimpleName) expr).getName())) { + return getFQNsFromType(var.getType()); + } + } + } + return null; + } + + /** + * Given an expression, try to find its type based on its surrounding context. For example, if an + * expression is located on the right-hand side of a variable declaration, take the type on the + * left. If an expression is passed into a known method, return the type of that parameter. This + * method will return null if the surrounding context's type cannot be found. + * + * @param expr The expression + * @param canRecurse Whether to allow recursion + * @return A set of FQNs, or null if unfound + */ + private @Nullable Set getFQNsFromSurroundingContextType( + Expression expr, boolean canRecurse) { + Node parentNode = expr.getParentNode().get(); + + // Method call, constructor call, super() call + if (parentNode instanceof NodeWithArguments) { + NodeWithArguments withArguments = (NodeWithArguments) parentNode; + + int param = -1; + for (int i = 0; i < withArguments.getArguments().size(); i++) { + if (withArguments.getArgument(i).equals(expr)) { + param = i; + } + } + + if (param != -1) { + try { + Object resolved = ((Resolvable) withArguments).resolve(); + // Constructors and methods both are ResolvedMethodLikeDeclaration + + if (resolved instanceof ResolvedMethodLikeDeclaration resolvedMethodLike) { + ResolvedType paramType = resolvedMethodLike.getParam(param).getType(); + + return Set.of(getFQNsForResolvedType(paramType)); + } + } catch (UnsolvedSymbolException ex) { + // Argument type is not resolvable; i.e., method is unsolvable + } + + CallableDeclaration singleCallable = + JavaParserUtil.tryFindSingleCallableForNodeWithUnresolvableArguments( + withArguments, fqnToCompilationUnits); + if (singleCallable != null) { + return Set.of(getFQNsFromType(singleCallable.getParameter(param).getType())); + } + + List> allPotentialCallables = + JavaParserUtil.tryResolveNodeWithUnresolvableArguments( + withArguments, fqnToCompilationUnits); + Set result = new LinkedHashSet<>(); + + for (CallableDeclaration callable : allPotentialCallables) { + result.add(getFQNsFromType(callable.getParameter(param).getType())); + } + + if (!result.isEmpty()) { + return result; + } + } + // scope of the method call, not an argument, continue + } else if (parentNode instanceof VariableDeclarator) { + VariableDeclarator declarator = (VariableDeclarator) parentNode; + + // When the parent is a VariableDeclarator, the child (expr) is on the right hand side + // The type is on the left hand side + if (!declarator.getType().isVarType()) { + return Set.of(getFQNsFromType(declarator.getType())); + } + } else if (parentNode instanceof AssignExpr && canRecurse) { + AssignExpr assignment = (AssignExpr) parentNode; + + // We could be on either side of the assignment operator + // In that case, take the type of the other side + + if (assignment.getTarget().equals(expr) && !assignment.getValue().isNullLiteralExpr()) { + return getFQNsForExpressionTypeImpl(assignment.getValue(), false); + } else if (assignment.getValue().equals(expr) + && !assignment.getTarget().isNullLiteralExpr()) { + return getFQNsForExpressionTypeImpl(assignment.getTarget(), false); + } + } + // Check if it's the conditional of an if, while, do, ?:; if so, its type is boolean + else if (parentNode instanceof NodeWithCondition withCondition) { + if (withCondition.getCondition().equals(expr)) { + return Set.of(new FullyQualifiedNameSet("boolean")); + } + + if (withCondition instanceof ConditionalExpr conditionalExpr && canRecurse) { + Expression other; + + if (conditionalExpr.getThenExpr().equals(expr)) { + other = conditionalExpr.getElseExpr(); + } else { + other = conditionalExpr.getThenExpr(); + } + + return getFQNsForExpressionTypeImpl(other, false); + } + } + // If it's in a binary expression (i.e., + - / * == != etc.), then set it to the type of the + // other side, if known + else if (parentNode instanceof BinaryExpr binary && canRecurse) { + Operator operator = binary.getOperator(); + + Expression other; + + if (binary.getLeft().equals(expr)) { + other = binary.getRight(); + } else { + other = binary.getLeft(); + } + + // Boolean + if (operator == BinaryExpr.Operator.AND || operator == BinaryExpr.Operator.OR) { + return Set.of(new FullyQualifiedNameSet("boolean")); + } else { + // Treat all other cases; type on one side is equal to the other + Set otherType = getFQNsForExpressionTypeImpl(other, false); + + // Safe to call isJavaLangOrPrimitiveName since any non-primitive/String type would be + // a synthetic type here + if (otherType.size() == 1 + && otherType.iterator().next().erasedFqns().size() == 1 + && JavaLangUtils.isJavaLangOrPrimitiveName( + otherType.iterator().next().erasedFqns().iterator().next())) { + return otherType; + } + + if (operator == BinaryExpr.Operator.EQUALS || operator == BinaryExpr.Operator.NOT_EQUALS) { + // ==, != work with any reference types, so we cannot know for certain the types on + // either side. Return the synthetic type generated above. + return otherType; + } + + // Try getting the type of the LHS; i.e. if looking at getA() + getB() in String x = + // getA() + getB(); + otherType = getFQNsForExpressionTypeImpl(binary, false); + + if (otherType.size() > 1 || otherType.iterator().next().erasedFqns().size() > 1) { + Set result = new LinkedHashSet<>(); + for (String validType : JavaLangUtils.getTypesForOp(operator.asString())) { + if (JavaParserUtil.isAClassName(validType)) { + validType = "java.lang." + validType; + } + result.add(new FullyQualifiedNameSet(validType)); + } + + return result; + } else { + return otherType; + } + } + } else if (parentNode instanceof ReturnStmt returnStmt) { + if (JavaParserUtil.findClosestMethodOrLambdaAncestor(returnStmt) + instanceof MethodDeclaration methodDecl) { + return Set.of(getFQNsFromType(methodDecl.getType())); + } + } else if (parentNode instanceof ForEachStmt) { + ForEachStmt forEachStmt = (ForEachStmt) parentNode; + + if (forEachStmt.getIterable().equals(expr)) { + FullyQualifiedNameSet notArray = + getFQNsFromType(forEachStmt.getVariable().getElementType()); + + Set result = new LinkedHashSet<>(); + for (String fqn : notArray.erasedFqns()) { + result.add(fqn + "[]"); + } + return Set.of(new FullyQualifiedNameSet(result, notArray.typeArguments())); + } + } + return null; + } + + /** + * Gets the FQNs of a type. + * + * @param type The type + * @return A set of FQNs or primitive names. + */ + public FullyQualifiedNameSet getFQNsFromType(Type type) { + // Unknown type is a lambda parameter: for example x in x -> (int)x + 1 + if (type.isUnknownType()) { + throw new RuntimeException("Do not pass in an unknown type to this method."); + } + + if (type.isArrayType()) { + Set result = new LinkedHashSet<>(); + int arrayLevel = type.asArrayType().getArrayLevel(); + FullyQualifiedNameSet elementFQNs = getFQNsFromType(type.asArrayType().getElementType()); + for (String fqn : elementFQNs.erasedFqns()) { + result.add(fqn + "[]".repeat(arrayLevel)); + } + + return new FullyQualifiedNameSet(result, elementFQNs.typeArguments()); + } + + if (type.isWildcardType()) { + if (type.asWildcardType().getExtendedType().isPresent()) { + FullyQualifiedNameSet extendedFQNs = + getFQNsFromType(type.asWildcardType().getExtendedType().get()); + + return new FullyQualifiedNameSet( + extendedFQNs.erasedFqns(), extendedFQNs.typeArguments(), "? extends"); + } else if (type.asWildcardType().getSuperType().isPresent()) { + FullyQualifiedNameSet superFQNs = + getFQNsFromType(type.asWildcardType().getSuperType().get()); + + return new FullyQualifiedNameSet( + superFQNs.erasedFqns(), superFQNs.typeArguments(), "? super"); + } else { + return FullyQualifiedNameSet.UNBOUNDED_WILDCARD; + } + } + + try { + ResolvedType resolved = type.resolve(); + + return getFQNsForResolvedType(resolved); + } catch (UnsolvedSymbolException ex) { + // continue + } + + if (type.isClassOrInterfaceType()) { + return getFQNsFromClassOrInterfaceType(type.asClassOrInterfaceType()); + } + + throw new RuntimeException("Unexpected type: " + type.getClass() + "; type value: " + type); + } + + /** + * Returns the possible FQNs of the type. Only use this method with unsolvable types; otherwise, + * use {@link #getFQNsFromType(Type)}. + * + * @param type The type + * @return A set of possible FQNs + */ + private FullyQualifiedNameSet getFQNsFromClassOrInterfaceType(ClassOrInterfaceType type) { + return getFQNsFromClassOrInterfaceTypeImpl(type, Set.of()); + } + + /** + * Returns the possible FQNs of the type. Only use this method with unsolvable types; otherwise, + * use {@link #getFQNsFromType(Type)}. Call this instead of {@link + * #getFQNsFromClassOrInterfaceType(ClassOrInterfaceType)} in {@link + * #getAllUnresolvableParentsImpl(TypeDeclaration, Node, Map, Set)}. + * + * @param type The type + * @param alreadyTraversed A set of type declarations that have already been traversed to prevent + * infinite recursion + * @return A set of possible FQNs + */ + private FullyQualifiedNameSet getFQNsFromClassOrInterfaceTypeImpl( + ClassOrInterfaceType type, Set> alreadyTraversed) { + // If a ClassOrInterfaceType is Map.Entry, we need to find the import with java.util.Map, not + // java.util.Map.Entry. + // Hence, look for the import with the "earliest" scope (with Map.Entry, this would be Map). + String getImportedName = type.getNameAsString(); + + Optional scope = type.getScope(); + + while (scope.isPresent()) { + getImportedName = scope.get().getNameAsString(); + scope = scope.get().getScope(); + } + + Set erasedFQNs = + getFQNsFromErasedClassNameImpl( + JavaParserUtil.erase(getImportedName), + JavaParserUtil.erase(type.getNameWithScope()), + type.findCompilationUnit().get(), + type, + alreadyTraversed); + + if (type.getTypeArguments().isPresent()) { + List typeArguments = new ArrayList<>(); + for (Type typeArg : type.getTypeArguments().get()) { + typeArguments.add(getFQNsFromType(typeArg)); + } + + return new FullyQualifiedNameSet(erasedFQNs, typeArguments); + } + + return new FullyQualifiedNameSet(erasedFQNs); + } + + /** + * Gets FQNs of an annotation. + * + * @param anno The annotation + * @return A set of possible FQNs + */ + public FullyQualifiedNameSet getFQNsFromAnnotation(AnnotationExpr anno) { + // If an annotation is @Foo.Bar, we need to find the import with org.example.Foo, not + // org.example.Foo.Bar. + // Hence, look for the import with the "earliest" scope (with @Foo.Bar, this would be Foo). + String getImportedName = anno.getNameAsString(); + + Optional scope = anno.getName().getQualifier(); + + while (scope.isPresent()) { + getImportedName = scope.get().asString(); + scope = scope.get().getQualifier(); + } + + TypeDeclaration parent = null; + + if (anno.getParentNode().isPresent()) { + // Instead of using the annotation, we use its parent. That is because the direct parent of + // an annotation is the declaration. If we did not use its annotation, the resulting class + // would not be in scope for the annotation. + parent = JavaParserUtil.getEnclosingClassLikeOptional(anno.getParentNode().get()); + } + + return new FullyQualifiedNameSet( + getFQNsFromErasedClassName( + getImportedName, anno.getNameAsString(), anno.findCompilationUnit().get(), parent)); + } + + /** + * Utility method for generating all possible fully-qualified names given the leftmost identifier + * of a class name, the full known name, and the node. Type names must be passed in as their + * erasures. + * + *

For example, if the class was Map.Entry (not fully qualified), the leftmost identifier would + * be Map, the full known name would be Map.Entry, and the node would be the ClassOrInterfaceType + * holding this value. + * + *

If node is null, then we will not generate FQNs for this class in unresolvable parent + * classes. + * + * @param firstIdentifier The leftmost identifier of the class name/class path + * @param fullName The full, known name of the class + * @param compilationUnit The compilation unit (we need this to be passed in because {@code node} + * could be null) + * @param node The node representing the class (if this is null, we won't look at parent types) + * @return A set of potential FQNs + */ + private Set getFQNsFromErasedClassName( + String firstIdentifier, + String fullName, + CompilationUnit compilationUnit, + @Nullable Node node) { + return getFQNsFromErasedClassNameImpl( + firstIdentifier, fullName, compilationUnit, node, Set.of()); + } + + /** + * Implementation for {@link #getFQNsFromErasedClassName(String, String, CompilationUnit, Node)}. + * Prevents infinite recursion by not looking at parent types if the parent type declaration has + * already been visited. + * + * @param firstIdentifier The leftmost identifier of the class name/class path + * @param fullName The full, known name of the class + * @param compilationUnit The compilation unit (we need this to be passed in because {@code node} + * could be null) + * @param node The node representing the class (if this is null, we won't look at parent types) + * @param alreadyTraversed A set of type declarations that have already been traversed to prevent + * infinite recursion + * @return A set of potential FQNs + */ + private Set getFQNsFromErasedClassNameImpl( + String firstIdentifier, + String fullName, + CompilationUnit compilationUnit, + @Nullable Node node, + Set> alreadyTraversed) { + Set fqns = new LinkedHashSet<>(); + + // If a class or interface type is unresolvable, it must be imported or be in the same package. + for (ImportDeclaration importDecl : compilationUnit.getImports()) { + if (importDecl.getNameAsString().endsWith("." + firstIdentifier)) { + return Set.of(importDecl.getName().getQualifier().get().toString() + "." + fullName); + } else if (importDecl.isAsterisk() + && !JavaLangUtils.inJdkPackage(importDecl.getNameAsString())) { + fqns.add(importDecl.getNameAsString() + "." + fullName); + } + } + + // Not imported + boolean shouldAddAfter = false; + if (JavaParserUtil.isAClassPath(fullName)) { + if (JavaParserUtil.isAClassName(firstIdentifier)) { + // Likely an inner class of another class, not a fully-qualified name; + // put the package FQN first so best effort generates that instead + shouldAddAfter = true; + } else { + // 1) fully qualified name + fqns.add(fullName); + return fqns; + } + + // 2) inner class of a parent class (i.e. Map.Entry), which could then fall under 3) and 4) + } + + // 3) in current package + Optional packageDecl = compilationUnit.getPackageDeclaration(); + if (packageDecl.isPresent()) { + fqns.add(packageDecl.get().getNameAsString() + "." + fullName); + + if (shouldAddAfter) { + fqns.add(fullName); + } + } else { + fqns.add(fullName); + } + + if (node != null) { + // 4) inner class of a parent class of the enclosing class + TypeDeclaration enclosingType = JavaParserUtil.getEnclosingClassLikeOptional(node); + + if (enclosingType != null && !alreadyTraversed.contains(enclosingType)) { + // If the node is a ClassOrInterfaceType, find the outermost node, since it could be a + // generic, which would cause a StackOverflowError + Node outerNode = node; + while (outerNode.hasParentNode() + && outerNode.getParentNode().get() instanceof ClassOrInterfaceType) { + outerNode = outerNode.getParentNode().get(); + } + + // Flatten the map: we only care about the value sets + for (Set set : getFQNsOfAllUnresolvableParents(enclosingType, outerNode).values()) { + for (String fqn : set) { + fqns.add(fqn + "." + fullName); + } + } + } + } + return fqns; + } + + /** + * Gets the FQN of the type of a statically imported field/method. + * + * @param expr the field access/method call expression to be used as input. Must be in the form of + * a qualified class name. + * @param isMethod true if the expression is a method call, false if it is a field access. + * @return The fully qualified name of the type of the statically imported member + */ + public static String generateFQNForTheTypeOfAStaticallyImportedMember( + String expr, boolean isMethod) { + // As this code involves complex string operations, we'll use a field access expression as an + // example, following its progression through the code. + // Suppose this is our field access expression: com.example.MyClass.myField + List fieldParts = Splitter.onPattern("[.]").splitToList(expr); + int numOfFieldParts = fieldParts.size(); + if (numOfFieldParts <= 2) { + throw new RuntimeException("Not in the form of a statically imported field."); + } + // this is the synthetic type of the field + StringBuilder fieldTypeClassName = new StringBuilder(toCapital(fieldParts.get(0))); + StringBuilder packageName = new StringBuilder(fieldParts.get(0)); + // According to the above example, fieldName will be myField + String fieldName = fieldParts.get(numOfFieldParts - 1); + @SuppressWarnings( + "signature") // this className is from the second-to-last part of a fully-qualified field + // signature, which is the simple name of a class. In this case, it is MyClass. + @ClassGetSimpleName String className = fieldParts.get(numOfFieldParts - 2); + // After this loop: fieldTypeClassName will be ComExample, and packageName will be com.example + for (int i = 1; i < numOfFieldParts - 2; i++) { + fieldTypeClassName.append(toCapital(fieldParts.get(i))); + packageName.append(".").append(fieldParts.get(i)); + } + // At this point, fieldTypeClassName will be ComExampleMyClassMyFieldSyntheticType + fieldTypeClassName + .append(toCapital(className)) + .append(toCapital(fieldName)) + .append(isMethod ? RETURN_TYPE : "SyntheticType"); + + return packageName.toString() + "." + fieldTypeClassName.toString(); + } + + /** + * Gets the fully qualified name of a functional interface type, given the number of parameters, + * whether the return type is void, and the context node for resolution. + * + * @param numberOfParams the number of parameters the functional interface should have + * @param isVoid true if the functional interface's method is void, false otherwise + * @param node any node used to get the compilation unit + * @return a FullyQualifiedNameSet representing the functional interface type + */ + public FullyQualifiedNameSet getQualifiedNameOfFunctionalInterface( + int numberOfParams, boolean isVoid, Node node) { + FullyQualifiedNameSet simple = getSimpleNameOfFunctionalInterface(numberOfParams, isVoid); + + String name = simple.erasedFqns().iterator().next(); + if (JavaParserUtil.isAClassPath(name)) { + return simple; + } + + return new FullyQualifiedNameSet( + getFQNsFromErasedClassName(name, name, node.findCompilationUnit().get(), node), + simple.typeArguments()); + } + + /** + * Gets the fully qualified name of a functional interface type, given a list of qualified + * parameter types, whether the return type is void, and the context node for resolution. + * + * @param parameters a list of fully qualified parameter types + * @param isVoid true if the functional interface's method is void, false otherwise + * @param node any node used to get the compilation unit + * @return a FullyQualifiedNameSet representing the functional interface type + */ + public FullyQualifiedNameSet getQualifiedNameOfFunctionalInterface( + List parameters, boolean isVoid, Node node) { + FullyQualifiedNameSet simple = getQualifiedNameOfFunctionalInterface(parameters, isVoid); + + String name = simple.erasedFqns().iterator().next(); + if (JavaParserUtil.isAClassPath(name)) { + return simple; + } + + return new FullyQualifiedNameSet( + getFQNsFromErasedClassName(name, name, node.findCompilationUnit().get(), node), + simple.typeArguments()); + } + + /** + * Gets the name of a functional interface type, given a list of qualified parameter types and the + * presence/absence of a return type. If a java.lang/util class can be used, then a FQN is + * returned; if not, then the simple class name is returned. + * + * @param parameters a list of qualified parameters + * @param isVoid true iff the method is void + * @return the fully-qualified name of a functional interface that is in-scope, matches the + * specified arity, and the specified voidness + */ + private static FullyQualifiedNameSet getQualifiedNameOfFunctionalInterface( + List parameters, boolean isVoid) { + parameters = new ArrayList<>(parameters); + + for (int i = 0; i < parameters.size(); i++) { + if (parameters.get(i).erasedFqns().size() == 1 + && JavaLangUtils.isPrimitive(parameters.get(i).erasedFqns().iterator().next())) { + parameters.set( + i, + new FullyQualifiedNameSet( + JavaLangUtils.getPrimitiveAsBoxedType( + parameters.get(i).erasedFqns().iterator().next()))); + } + } + + // check arity: + int numberOfParams = parameters.size(); + if (numberOfParams == 0 && isVoid) { + return new FullyQualifiedNameSet("java.lang.Runnable"); + } else if (numberOfParams == 0 && !isVoid) { + return new FullyQualifiedNameSet( + Set.of("java.util.function.Supplier"), List.of(FullyQualifiedNameSet.UNBOUNDED_WILDCARD)); + } else if (numberOfParams == 1 && isVoid) { + return new FullyQualifiedNameSet(Set.of("java.util.function.Consumer"), parameters); + } else if (numberOfParams == 1 && !isVoid) { + return new FullyQualifiedNameSet( + Set.of("java.util.function.Function"), + List.of(parameters.get(0), FullyQualifiedNameSet.UNBOUNDED_WILDCARD)); + } else if (numberOfParams == 2 && isVoid) { + return new FullyQualifiedNameSet(Set.of("java.util.function.BiConsumer"), parameters); + } else if (numberOfParams == 2 && !isVoid) { + return new FullyQualifiedNameSet( + Set.of("java.util.function.BiFunction"), + List.of(parameters.get(0), parameters.get(1), FullyQualifiedNameSet.UNBOUNDED_WILDCARD)); + } else { + String funcInterfaceName = + isVoid ? "SyntheticConsumer" + numberOfParams : "SyntheticFunction" + numberOfParams; + + if (!isVoid) { + List typeArgs = new ArrayList<>(parameters); + typeArgs.add(FullyQualifiedNameSet.UNBOUNDED_WILDCARD); + + parameters = typeArgs; + } + + return new FullyQualifiedNameSet(Set.of(funcInterfaceName), parameters); + } + } + + /** + * Gets the name of a functional interface type, given the number of parameters and the + * presence/absence of a return type. If a java.lang/util class can be used, then a FQN is + * returned; if not, then the simple class name is returned. + * + * @param numberOfParams the number of parameters + * @param isVoid true iff the method is void + * @return the fully-qualified name of a functional interface that is in-scope, matches the + * specified arity, and the specified voidness + */ + private static FullyQualifiedNameSet getSimpleNameOfFunctionalInterface( + int numberOfParams, boolean isVoid) { + List parameters = new ArrayList<>(numberOfParams); + for (int i = 0; i < numberOfParams; i++) { + parameters.add(FullyQualifiedNameSet.UNBOUNDED_WILDCARD); + } + + return getQualifiedNameOfFunctionalInterface(parameters, isVoid); + } + + /** + * Gets all FQNs of parents (such as implements and extends) recursively, given a type + * declaration. + * + * @param typeDecl the type declaration + * @param currentNode the current node. If the current node is found to be one of the + * implemented/extended types, then we will not go down that path. + * @return A map of all class/interface FQNs representing all {@code typeDecl}'s parents; simple + * class name --> set of potential FQNs + */ + public Map> getFQNsOfAllUnresolvableParents( + TypeDeclaration typeDecl, Node currentNode) { + Map> map = new LinkedHashMap<>(); + Set> traversedTypeDeclarations = new HashSet<>(); + + getAllUnresolvableParentsImpl(typeDecl, currentNode, map, traversedTypeDeclarations); + + return map; + } + + /** + * Helper method for {@link #getFQNsOfAllUnresolvableParents(TypeDeclaration, Node)}. This method + * recursively calls itself on resolvable class/interface declarations, and continues to add fqns + * to the map. + * + * @param typeDecl The type declaration to find parents from + * @param currentNode The current node, to determine whether or not we should continue down that + * path (if node.equals(currentNode), do not do recurse) + * @param map The map to add to + * @param traversedTypeDeclarations A set of type declarations that have already been traversed to + * avoid infinite recursion + */ + private void getAllUnresolvableParentsImpl( + TypeDeclaration typeDecl, + Node currentNode, + Map> map, + Set> traversedTypeDeclarations) { + if (traversedTypeDeclarations.contains(typeDecl)) { + return; + } + traversedTypeDeclarations.add(typeDecl); + + if (typeDecl instanceof NodeWithImplements) { + for (ClassOrInterfaceType type : ((NodeWithImplements) typeDecl).getImplementedTypes()) { + if (type.equals(currentNode)) { + continue; + } + + try { + ResolvedReferenceType resolved = type.resolve().asReferenceType(); + TypeDeclaration parentTypeDecl = + JavaParserUtil.getTypeFromQualifiedName( + resolved.getQualifiedName(), fqnToCompilationUnits); + + if (parentTypeDecl != null) { + getAllUnresolvableParentsImpl( + parentTypeDecl, currentNode, map, traversedTypeDeclarations); + } + + } catch (UnsolvedSymbolException ex) { + map.put( + type.getNameWithScope(), + getFQNsFromClassOrInterfaceTypeImpl(type, traversedTypeDeclarations).erasedFqns()); + } + } + } + + if (typeDecl instanceof NodeWithExtends) { + for (ClassOrInterfaceType type : ((NodeWithExtends) typeDecl).getExtendedTypes()) { + if (type.equals(currentNode)) { + continue; + } + + try { + ResolvedReferenceType resolved = type.resolve().asReferenceType(); + TypeDeclaration parentTypeDecl = + JavaParserUtil.getTypeFromQualifiedName( + resolved.getQualifiedName(), fqnToCompilationUnits); + + if (parentTypeDecl != null) { + getAllUnresolvableParentsImpl( + parentTypeDecl, currentNode, map, traversedTypeDeclarations); + } + + } catch (UnsolvedSymbolException ex) { + map.put( + type.getNameWithScope(), + getFQNsFromClassOrInterfaceTypeImpl(type, traversedTypeDeclarations).erasedFqns()); + } + } + } + } + + /** + * This method capitalizes a string. For example, "hello" will become "Hello". + * + * @param string the string to be capitalized + * @return the capitalized version of the string + */ + private static String toCapital(String string) { + return Ascii.toUpperCase(string.substring(0, 1)) + string.substring(1); + } +} diff --git a/src/main/java/org/checkerframework/specimin/unsolved/FullyQualifiedNameSet.java b/src/main/java/org/checkerframework/specimin/unsolved/FullyQualifiedNameSet.java new file mode 100644 index 000000000..381b05cd6 --- /dev/null +++ b/src/main/java/org/checkerframework/specimin/unsolved/FullyQualifiedNameSet.java @@ -0,0 +1,68 @@ +package org.checkerframework.specimin.unsolved; + +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import org.checkerframework.checker.nullness.qual.Nullable; + +/** + * Represents a set of fully qualified names from FullyQualifiedNameGenerator, representing a single + * type. This record also holds type arguments and a wildcard if applicable. The parameter for + * wildcard should hold either "?", "? extends", or "? super". + * + *

For example, if representing the set {@code [? extends org.example.A, ? extends + * com.example.A]}, then pass in a set of erasedFqns {@code [org.example.A, + * com.example.A]}, a list of FullyQualifiedNameSet {@code [org.example.B, com.example.B]} for type + * arguments, and a wildcard of {@code ? extends}. + */ +public record FullyQualifiedNameSet( + Set erasedFqns, List typeArguments, @Nullable String wildcard) { + + /** Represents an unbounded wildcard: ? */ + public static final FullyQualifiedNameSet UNBOUNDED_WILDCARD = + new FullyQualifiedNameSet(Set.of(), List.of(), "?"); + + /** + * Creates a FullyQualifiedNameSet with erased FQNs, type arguments, but no wildcard. + * + * @param erasedFqns A set of erased fully qualified names. + * @param typeArguments A list of type arguments + */ + public FullyQualifiedNameSet(Set erasedFqns, List typeArguments) { + this(erasedFqns, typeArguments, null); + } + + /** + * Creates a FullyQualifiedNameSet with erased FQNs and no type arguments. + * + * @param erasedFqns A set of erased fully qualified names. + */ + public FullyQualifiedNameSet(Set erasedFqns) { + this(erasedFqns, Collections.emptyList(), null); + } + + /** + * Creates a FullyQualifiedNameSet with erased FQNs and no type arguments. + * + * @param erasedFqns A varargs of erased fully qualified names. + */ + public FullyQualifiedNameSet(String... erasedFqns) { + this(Set.of(erasedFqns)); + } + + @Override + public boolean equals(@Nullable Object other) { + if (other instanceof FullyQualifiedNameSet otherSet) { + return Objects.equals(erasedFqns, otherSet.erasedFqns) + && Objects.equals(typeArguments, otherSet.typeArguments) + && Objects.equals(wildcard, otherSet.wildcard); + } + return false; + } + + @Override + public int hashCode() { + return Objects.hash(erasedFqns, typeArguments, wildcard); + } +} diff --git a/src/main/java/org/checkerframework/specimin/unsolved/FunctionalInterfaceHelper.java b/src/main/java/org/checkerframework/specimin/unsolved/FunctionalInterfaceHelper.java new file mode 100644 index 000000000..695004d3c --- /dev/null +++ b/src/main/java/org/checkerframework/specimin/unsolved/FunctionalInterfaceHelper.java @@ -0,0 +1,254 @@ +package org.checkerframework.specimin.unsolved; + +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.stream.Stream; +import org.checkerframework.checker.nullness.qual.Nullable; + +/** Provides helper methods for handling functional interfaces. */ +public class FunctionalInterfaceHelper { + /** + * Converts any java.util.function functional interface to a "normal" functional interface, i.e., + * Runnable, Supplier, Consumer, Function, BiConsumer, BiFunction. BooleanSupplier is changed to + * {@code Supplier}, DoubleToIntFunction to {@code Function}, etc. + * + * @param fqnSet The set of fully qualified names to convert + * @return A new set with all functional interfaces converted to their "normal" forms + */ + public static FullyQualifiedNameSet convertToNormalFunctionalInterface( + FullyQualifiedNameSet fqnSet) { + // See https://docs.oracle.com/javase/8/docs/api/java/util/function/package-summary.html + String fqn = fqnSet.erasedFqns().iterator().next(); + + if (!fqn.startsWith("java.util.function.")) { + return fqnSet; + } + + String name = fqn.substring("java.util.function.".length()); + + // Autogenerated with GitHub Copilot using the contents from the link above + return switch (name) { + case "BinaryOperator" -> + new FullyQualifiedNameSet( + Set.of("java.util.function.BiFunction"), + Collections.nCopies(3, fqnSet.typeArguments().get(0))); + case "BiPredicate" -> + new FullyQualifiedNameSet( + Set.of("java.util.function.BiFunction"), + Stream.concat( + fqnSet.typeArguments().stream(), + Stream.of(new FullyQualifiedNameSet("java.lang.Boolean"))) + .toList()); + case "BooleanSupplier" -> + new FullyQualifiedNameSet( + Set.of("java.util.function.Supplier"), + List.of(new FullyQualifiedNameSet("java.lang.Boolean"))); + case "DoubleBinaryOperator" -> + new FullyQualifiedNameSet( + Set.of("java.util.function.BiFunction"), + Collections.nCopies(3, new FullyQualifiedNameSet("java.lang.Double"))); + case "DoubleConsumer" -> + new FullyQualifiedNameSet( + Set.of("java.util.function.Consumer"), + List.of(new FullyQualifiedNameSet("java.lang.Double"))); + case "DoubleFunction" -> + new FullyQualifiedNameSet( + Set.of("java.util.function.Function"), + List.of( + new FullyQualifiedNameSet("java.lang.Double"), fqnSet.typeArguments().get(0))); + case "DoublePredicate" -> + new FullyQualifiedNameSet( + Set.of("java.util.function.Function"), + List.of( + new FullyQualifiedNameSet("java.lang.Double"), + new FullyQualifiedNameSet("java.lang.Boolean"))); + case "DoubleSupplier" -> + new FullyQualifiedNameSet( + Set.of("java.util.function.Supplier"), + List.of(new FullyQualifiedNameSet("java.lang.Double"))); + case "DoubleToIntFunction" -> + new FullyQualifiedNameSet( + Set.of("java.util.function.Function"), + List.of( + new FullyQualifiedNameSet("java.lang.Double"), + new FullyQualifiedNameSet("java.lang.Integer"))); + case "DoubleToLongFunction" -> + new FullyQualifiedNameSet( + Set.of("java.util.function.Function"), + List.of( + new FullyQualifiedNameSet("java.lang.Double"), + new FullyQualifiedNameSet("java.lang.Long"))); + case "DoubleUnaryOperator" -> + new FullyQualifiedNameSet( + Set.of("java.util.function.Function"), + List.of( + new FullyQualifiedNameSet("java.lang.Double"), + new FullyQualifiedNameSet("java.lang.Double"))); + case "IntBinaryOperator" -> + new FullyQualifiedNameSet( + Set.of("java.util.function.BiFunction"), + Collections.nCopies(3, new FullyQualifiedNameSet("java.lang.Integer"))); + case "IntConsumer" -> + new FullyQualifiedNameSet( + Set.of("java.util.function.Consumer"), + List.of(new FullyQualifiedNameSet("java.lang.Integer"))); + case "IntFunction" -> + new FullyQualifiedNameSet( + Set.of("java.util.function.Function"), + List.of( + new FullyQualifiedNameSet("java.lang.Integer"), fqnSet.typeArguments().get(0))); + case "IntPredicate" -> + new FullyQualifiedNameSet( + Set.of("java.util.function.Function"), + List.of( + new FullyQualifiedNameSet("java.lang.Integer"), + new FullyQualifiedNameSet("java.lang.Boolean"))); + case "IntSupplier" -> + new FullyQualifiedNameSet( + Set.of("java.util.function.Supplier"), + List.of(new FullyQualifiedNameSet("java.lang.Integer"))); + case "IntToDoubleFunction" -> + new FullyQualifiedNameSet( + Set.of("java.util.function.Function"), + List.of( + new FullyQualifiedNameSet("java.lang.Integer"), + new FullyQualifiedNameSet("java.lang.Double"))); + case "IntToLongFunction" -> + new FullyQualifiedNameSet( + Set.of("java.util.function.Function"), + List.of( + new FullyQualifiedNameSet("java.lang.Integer"), + new FullyQualifiedNameSet("java.lang.Long"))); + case "IntUnaryOperator" -> + new FullyQualifiedNameSet( + Set.of("java.util.function.Function"), + List.of( + new FullyQualifiedNameSet("java.lang.Integer"), + new FullyQualifiedNameSet("java.lang.Integer"))); + case "LongBinaryOperator" -> + new FullyQualifiedNameSet( + Set.of("java.util.function.BiFunction"), + Collections.nCopies(3, new FullyQualifiedNameSet("java.lang.Long"))); + case "LongConsumer" -> + new FullyQualifiedNameSet( + Set.of("java.util.function.Consumer"), + List.of(new FullyQualifiedNameSet("java.lang.Long"))); + case "LongFunction" -> + new FullyQualifiedNameSet( + Set.of("java.util.function.Function"), + List.of(new FullyQualifiedNameSet("java.lang.Long"), fqnSet.typeArguments().get(0))); + case "LongPredicate" -> + new FullyQualifiedNameSet( + Set.of("java.util.function.Function"), + List.of( + new FullyQualifiedNameSet("java.lang.Long"), + new FullyQualifiedNameSet("java.lang.Boolean"))); + case "LongSupplier" -> + new FullyQualifiedNameSet( + Set.of("java.util.function.Supplier"), + List.of(new FullyQualifiedNameSet("java.lang.Long"))); + case "LongToDoubleFunction" -> + new FullyQualifiedNameSet( + Set.of("java.util.function.Function"), + List.of( + new FullyQualifiedNameSet("java.lang.Long"), + new FullyQualifiedNameSet("java.lang.Double"))); + case "LongToIntFunction" -> + new FullyQualifiedNameSet( + Set.of("java.util.function.Function"), + List.of( + new FullyQualifiedNameSet("java.lang.Long"), + new FullyQualifiedNameSet("java.lang.Integer"))); + case "LongUnaryOperator" -> + new FullyQualifiedNameSet( + Set.of("java.util.function.Function"), + List.of( + new FullyQualifiedNameSet("java.lang.Long"), + new FullyQualifiedNameSet("java.lang.Long"))); + case "ObjDoubleConsumer" -> + new FullyQualifiedNameSet( + Set.of("java.util.function.BiConsumer"), + List.of( + fqnSet.typeArguments().get(0), new FullyQualifiedNameSet("java.lang.Double"))); + case "ObjIntConsumer" -> + new FullyQualifiedNameSet( + Set.of("java.util.function.BiConsumer"), + List.of( + fqnSet.typeArguments().get(0), new FullyQualifiedNameSet("java.lang.Integer"))); + case "ObjLongConsumer" -> + new FullyQualifiedNameSet( + Set.of("java.util.function.BiConsumer"), + List.of(fqnSet.typeArguments().get(0), new FullyQualifiedNameSet("java.lang.Long"))); + case "Predicate" -> + new FullyQualifiedNameSet( + Set.of("java.util.function.Function"), + List.of( + fqnSet.typeArguments().get(0), new FullyQualifiedNameSet("java.lang.Boolean"))); + case "ToDoubleBiFunction" -> + new FullyQualifiedNameSet( + Set.of("java.util.function.BiFunction"), + List.of( + fqnSet.typeArguments().get(0), + fqnSet.typeArguments().get(1), + new FullyQualifiedNameSet("java.lang.Double"))); + case "ToDoubleFunction" -> + new FullyQualifiedNameSet( + Set.of("java.util.function.Function"), + List.of( + fqnSet.typeArguments().get(0), new FullyQualifiedNameSet("java.lang.Double"))); + case "ToIntBiFunction" -> + new FullyQualifiedNameSet( + Set.of("java.util.function.BiFunction"), + List.of( + fqnSet.typeArguments().get(0), + fqnSet.typeArguments().get(1), + new FullyQualifiedNameSet("java.lang.Integer"))); + case "ToIntFunction" -> + new FullyQualifiedNameSet( + Set.of("java.util.function.Function"), + List.of( + fqnSet.typeArguments().get(0), new FullyQualifiedNameSet("java.lang.Integer"))); + case "ToLongBiFunction" -> + new FullyQualifiedNameSet( + Set.of("java.util.function.BiFunction"), + List.of( + fqnSet.typeArguments().get(0), + fqnSet.typeArguments().get(1), + new FullyQualifiedNameSet("java.lang.Long"))); + case "ToLongFunction" -> + new FullyQualifiedNameSet( + Set.of("java.util.function.Function"), + List.of(fqnSet.typeArguments().get(0), new FullyQualifiedNameSet("java.lang.Long"))); + case "UnaryOperator" -> + new FullyQualifiedNameSet( + Set.of("java.util.function.Function"), + List.of(fqnSet.typeArguments().get(0), fqnSet.typeArguments().get(0))); + default -> fqnSet; + }; + } + + /** + * Given a normalized functional interface from {@link + * #convertToNormalFunctionalInterface(FullyQualifiedNameSet)}, get the return type of the + * functional method. Returns null if void. + * + * @param normalized The normalized functional interface + * @return The return type of the functional method, or null if void + */ + public static @Nullable FullyQualifiedNameSet getReturnTypeFromNormalizedFunctionalInterface( + FullyQualifiedNameSet normalized) { + String fqn = normalized.erasedFqns().iterator().next(); + + if (fqn.equals("java.util.function.Supplier") + || fqn.equals("java.util.function.Function") + || fqn.equals("java.util.function.BiFunction")) { + List typeArgs = normalized.typeArguments(); + if (!typeArgs.isEmpty()) { + return typeArgs.get(typeArgs.size() - 1); + } + } + + return null; + } +} diff --git a/src/main/java/org/checkerframework/specimin/unsolved/MemberType.java b/src/main/java/org/checkerframework/specimin/unsolved/MemberType.java new file mode 100644 index 000000000..b152dabd5 --- /dev/null +++ b/src/main/java/org/checkerframework/specimin/unsolved/MemberType.java @@ -0,0 +1,50 @@ +package org.checkerframework.specimin.unsolved; + +import java.util.List; +import java.util.Set; + +/** + * Class to represent the type of an unsolved field, the return type of an unsolved method, or the + * types of an unsolved method's parameters. + * + *

Use this class instead of hardcoding a string into {@link UnsolvedMethod} or {@link + * UnsolvedField} to ensure proper types when alternates are generated. + */ +public abstract class MemberType { + /** The type arguments of this type. */ + private List typeArguments; + + /** + * Creates a new MemberType with the given type arguments. + * + * @param typeArguments The type arguments for this MemberType, which can be empty if there are + * none. + */ + public MemberType(List typeArguments) { + this.typeArguments = typeArguments; + } + + /** + * Gets the set of fully qualified names for this type. + * + * @return The set of fully qualified names representing this type + */ + public abstract Set getFullyQualifiedNames(); + + /** + * Gets the type arguments for this type. + * + * @return The list of member types representing the type arguments of this type + */ + public List getTypeArguments() { + return typeArguments; + } + + /** + * Utility method to return a copy of this member type with new type arguments. + * + * @param newTypeArgs The new type arguments to use for the copy + * @return A copy of this member type with the specified type arguments + */ + public abstract MemberType copyWithNewTypeArgs(List newTypeArgs); +} diff --git a/src/main/java/org/checkerframework/specimin/unsolved/SolvedMemberType.java b/src/main/java/org/checkerframework/specimin/unsolved/SolvedMemberType.java new file mode 100644 index 000000000..34d538143 --- /dev/null +++ b/src/main/java/org/checkerframework/specimin/unsolved/SolvedMemberType.java @@ -0,0 +1,105 @@ +package org.checkerframework.specimin.unsolved; + +import java.util.List; +import java.util.Objects; +import java.util.Set; +import org.checkerframework.checker.nullness.qual.Nullable; + +/** + * Represents a {@link MemberType} of a solved symbol, wrapping around a single String (FQN). {@link + * #getFullyQualifiedNames()} always returns a set containing only the FQN. + * + *

See {@link MemberType} for more details. + */ +public class SolvedMemberType extends MemberType { + /** Represents java.lang.Exception */ + public static final SolvedMemberType JAVA_LANG_EXCEPTION = + new SolvedMemberType("java.lang.Exception"); + + /** Represents java.lang.Error */ + public static final SolvedMemberType JAVA_LANG_ERROR = new SolvedMemberType("java.lang.Error"); + + /** Represents java.lang.Object */ + public static final SolvedMemberType JAVA_LANG_OBJECT = new SolvedMemberType("java.lang.Object"); + + /** The fully-qualified name represented by this type. */ + private String fqn; + + /** + * Creates a new SolvedMemberType based on a fully-qualified name. May include array brackets. + * + * @param fqn The fully-qualified name + */ + public SolvedMemberType(String fqn) { + this(fqn, List.of()); + } + + /** + * Creates a new SolvedMemberType based on a fully-qualified name. May include array brackets. + * Also provides a list of type arguments. + * + * @param fqn The fully-qualified name + * @param typeArguments The type arguments for this type + */ + public SolvedMemberType(String fqn, List typeArguments) { + super(typeArguments); + this.fqn = fqn; + } + + @Override + public Set getFullyQualifiedNames() { + return Set.of(fqn); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + + int arrayBracketsIndex = fqn.indexOf("[]"); + String arrayBrackets; + if (arrayBracketsIndex != -1) { + arrayBrackets = fqn.substring(arrayBracketsIndex); + sb.append(fqn.substring(0, arrayBracketsIndex)); + } else { + arrayBrackets = ""; + sb.append(fqn); + } + + if (!getTypeArguments().isEmpty()) { + sb.append('<'); + for (int i = 0; i < getTypeArguments().size(); i++) { + if (i > 0) { + sb.append(", "); + } + sb.append(getTypeArguments().get(i).toString()); + } + sb.append('>'); + } + + if (!arrayBrackets.isBlank()) { + sb.append(arrayBrackets); + } + + return sb.toString(); + } + + @Override + public boolean equals(@Nullable Object other) { + if (!(other instanceof SolvedMemberType otherAsSolvedMemberType)) { + return false; + } + + return Objects.equals(otherAsSolvedMemberType.fqn, this.fqn) + && Objects.equals(otherAsSolvedMemberType.getTypeArguments(), this.getTypeArguments()); + } + + @Override + public int hashCode() { + return Objects.hash(fqn, getTypeArguments()); + } + + @Override + public MemberType copyWithNewTypeArgs(List newTypeArgs) { + return new SolvedMemberType(fqn, newTypeArgs); + } +} diff --git a/src/main/java/org/checkerframework/specimin/unsolved/UnsolvedClassOrInterface.java b/src/main/java/org/checkerframework/specimin/unsolved/UnsolvedClassOrInterface.java new file mode 100644 index 000000000..24cca2350 --- /dev/null +++ b/src/main/java/org/checkerframework/specimin/unsolved/UnsolvedClassOrInterface.java @@ -0,0 +1,350 @@ +package org.checkerframework.specimin.unsolved; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.checker.signature.qual.ClassGetSimpleName; + +/** Represents a single unsolved class or interface alternate. */ +public class UnsolvedClassOrInterface extends UnsolvedSymbolAlternate + implements UnsolvedClassOrInterfaceCommon { + + /** The name of the class */ + private final @ClassGetSimpleName String className; + + /** + * The name of the package of the class. We rely on the import statements from the source codes to + * guess the package name. + */ + private final String packageName; + + /** The type variables, if any exist. */ + private List typeVariables = Collections.emptyList(); + + /** The extends clause, if one exists. */ + private @Nullable MemberType extendsClause; + + /** The implements clauses, if they exist. */ + private Set implementsClauses = new LinkedHashSet<>(0); + + /** The annotations on this type. */ + private Set annotations = new HashSet<>(); + + /** The type of this type; i.e., is it a class, interface, annotation, enum? */ + private UnsolvedClassOrInterfaceType typeOfType = UnsolvedClassOrInterfaceType.UNKNOWN; + + /** + * This constructor correctly splits apart the class name and any generics attached to it. + * + * @param className the name of the class, possibly followed by a set of type arguments + * @param packageName the name of the package + */ + public UnsolvedClassOrInterface(String className, String packageName) { + // Types do not have mustPreserve nodes + super(Set.of()); + if (className.contains("<")) { + @SuppressWarnings("signature") // removing the <> makes this a true simple name + @ClassGetSimpleName String classNameWithoutAngleBrackets = className.substring(0, className.indexOf('<')); + this.className = classNameWithoutAngleBrackets; + } else { + @SuppressWarnings("signature") // no angle brackets means this is a true simple name + @ClassGetSimpleName String classNameWithoutAngleBrackets = className; + this.className = classNameWithoutAngleBrackets; + } + this.packageName = packageName; + } + + /** + * Get the name of this class (note: without any generic type variables). + * + * @return the name of the class + */ + @Override + public @ClassGetSimpleName String getClassName() { + return className; + } + + /** + * Return the qualified name of this class. + * + * @return the qualified name + */ + public String getFullyQualifiedName() { + return packageName + "." + className; + } + + /** + * Get the package where this class belongs to + * + * @return the value of packageName + */ + public String getPackageName() { + return packageName; + } + + /** + * Adds new interfaces to the set of implemented interfaces. + * + * @param interfaceTypes The interface types + */ + public void implement(Collection interfaceTypes) { + implementsClauses.addAll(interfaceTypes); + } + + /** + * Adds a new interface to the set of implemented interfaces. + * + * @param interfaceType The interface type + */ + public void implement(MemberType interfaceType) { + implementsClauses.add(interfaceType); + } + + /** + * Checks if an interface is implemented or not. + * + * @param interfaceType the fqn of the interface + */ + @Override + public boolean doesImplement(MemberType interfaceType) { + return implementsClauses.contains(interfaceType); + } + + /** + * Adds an extends clause to this class. + * + * @param extendsType a {@link MemberType} of the extended type, represented with fully qualified + * names. + */ + public void extend(MemberType extendsType) { + this.extendsClause = extendsType; + } + + /** + * Returns true if this class extends another class. + * + * @return whether {@code this.extendsClause} is non-null + */ + @Override + public boolean hasExtends() { + return this.extendsClause != null; + } + + /** + * Checks if the extended class is equal to the input. + * + * @param extendsType a fully-qualified class name for the extended class + * @return whether {@code className} is the extended class of this + */ + @Override + public boolean doesExtend(MemberType extendsType) { + return this.extendsClause != null && this.extendsClause.equals(extendsType); + } + + /** + * Adds an annotation to this class. + * + * @param annotation a fully-qualified annotation to apply + */ + @Override + public void addAnnotation(String annotation) { + this.annotations.add(annotation); + } + + @Override + public boolean equals(@Nullable Object other) { + if (!(other instanceof UnsolvedClassOrInterface)) { + return false; + } + UnsolvedClassOrInterface otherClass = (UnsolvedClassOrInterface) other; + // Note: an UnsovledClass cannot represent an anonymous class + // (each UnsolvedClass corresponds to a source file), so this + // check is sufficient for equality (it is checking the canonical name). + return otherClass.className.equals(this.className) + && otherClass.packageName.equals(this.packageName); + } + + @Override + public int hashCode() { + return Objects.hash(className, packageName); + } + + /** + * Returns a copy of this class. + * + * @return A copy of the current instance + */ + public UnsolvedClassOrInterface copy() { + UnsolvedClassOrInterface copy = new UnsolvedClassOrInterface(className, packageName); + + copy.extendsClause = this.extendsClause; + copy.implementsClauses = new LinkedHashSet<>(this.implementsClauses); + copy.typeOfType = this.typeOfType; + copy.typeVariables = new ArrayList<>(this.typeVariables); + copy.annotations = new HashSet<>(this.annotations); + + return copy; + } + + @Override + public String toString() { + return toString( + Collections.emptyList(), Collections.emptyList(), Collections.emptyList(), false); + } + + /** + * Return the content of the class as a compilable Java file. + * + * @param methods the methods of the class + * @param fields the fields of the class + * @param innerClassDefinitions the inner classes of the class + * @param isInnerClass whether this class is an inner class + * @return the content of the class + */ + public String toString( + Collection methods, + Collection fields, + Collection innerClassDefinitions, + boolean isInnerClass) { + StringBuilder sb = new StringBuilder(); + if (!isInnerClass) { + sb.append("package ").append(packageName).append(";\n"); + } + + for (String annotation : annotations) { + sb.append(annotation).append("\n"); + } + + sb.append("public "); + if (isInnerClass) { + // Nested classes that are visible outside their parent class + // are usually static. There is no downside to making them static + // (it imposes no additional requirements), but there is a downside + // to making them non-static (they must be attached to a specific member + // of the outer class, which may or may not be true in the event). + // TODO: I'm not sure we actually have test cases for "real" inner classes + // (which are non-static nested classes). All of our "inner class" tests + // appear to be intended for static nested classes. See + // https://docs.oracle.com/javase/tutorial/java/javaOO/nested.html for + // a discussion of the difference. + sb.append("static "); + } + if (typeOfType == UnsolvedClassOrInterfaceType.INTERFACE) { + sb.append("interface "); + } else if (typeOfType == UnsolvedClassOrInterfaceType.ANNOTATION) { + sb.append("@interface "); + } else if (typeOfType == UnsolvedClassOrInterfaceType.ENUM) { + sb.append("enum "); + } else { + sb.append("class "); + } + sb.append(className); + + if (!getTypeVariables().isEmpty()) { + sb.append("<"); + sb.append(String.join(", ", getTypeVariables())); + sb.append(">"); + } + + if (extendsClause != null) { + @NonNull MemberType nonNullExtends = extendsClause; + sb.append(" extends ").append(nonNullExtends).append(" "); + } + if (implementsClauses.size() > 0) { + if (typeOfType == UnsolvedClassOrInterfaceType.INTERFACE) { + if (extendsClause != null) { + sb.append(", "); + } else { + sb.append(" extends "); + } + } else { + sb.append(" implements "); + } + Iterator interfaces = implementsClauses.iterator(); + while (interfaces.hasNext()) { + sb.append(interfaces.next()); + if (interfaces.hasNext()) { + sb.append(", "); + } + } + } + sb.append(" {\n"); + if (innerClassDefinitions != null) { + for (String innerClass : innerClassDefinitions) { + sb.append(innerClass); + } + } + for (UnsolvedField variableDeclarations : fields) { + sb.append(" ").append(variableDeclarations.toString(typeOfType)).append("\n"); + } + for (UnsolvedMethod method : methods) { + sb.append(method.toString(typeOfType)); + } + sb.append("}\n"); + return sb.toString(); + } + + /** + * Return a synthetic representation for type variables of the current class, without surrounding + * angle brackets. + * + * @return the synthetic representation for type variables + */ + @Override + public List getTypeVariables() { + return typeVariables; + } + + @Override + public UnsolvedClassOrInterfaceType getType() { + return typeOfType; + } + + /** + * Gets the set of types that are implemented by this type. + * + * @return The implemented types + */ + public Set getImplementedTypes() { + return implementsClauses; + } + + /** + * Gets the extended type of this type. + * + * @return The extended type, or null if none + */ + public @Nullable MemberType getExtendedType() { + return extendsClause; + } + + @Override + public void setType(UnsolvedClassOrInterfaceType type) { + typeOfType = type; + } + + @Override + public void setTypeVariables(List typeVariables) { + this.typeVariables = typeVariables; + } + + @Override + public void setTypeVariables(int numberOfTypeVariables) { + List result = new ArrayList<>(); + + for (int i = 0; i < numberOfTypeVariables; i++) { + String typeExpression = "T" + ((i > 0) ? i : ""); + result.add(typeExpression); + } + + setTypeVariables(result); + } +} diff --git a/src/main/java/org/checkerframework/specimin/unsolved/UnsolvedClassOrInterfaceAlternates.java b/src/main/java/org/checkerframework/specimin/unsolved/UnsolvedClassOrInterfaceAlternates.java new file mode 100644 index 000000000..22600e9e4 --- /dev/null +++ b/src/main/java/org/checkerframework/specimin/unsolved/UnsolvedClassOrInterfaceAlternates.java @@ -0,0 +1,804 @@ +package org.checkerframework.specimin.unsolved; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.checker.signature.qual.ClassGetSimpleName; +import org.checkerframework.specimin.JavaParserUtil; + +/** + * Given a set of fully qualified type name, hold all possible type definitions. For example, if + * given a set containing FQNs like org.example.Apple.Banana, generate alternates where it can be + * any of the following: + * + *

    + *
  • class Banana in package org.example.Apple + *
  • inner class Banana in class Apple in package org.example + *
  • inner class Banana in inner class Apple in class example in package org + *
+ */ +public class UnsolvedClassOrInterfaceAlternates + extends UnsolvedSymbolAlternates + implements UnsolvedClassOrInterfaceCommon { + /** + * Represents a relationship between this type to a super type. If we find eventually that this + * UnsolvedClassOrInterfaceAlternates is an interface, all these relationships do not matter since + * all interfaces can only extend other interfaces. This enum is primarily used for + * SolvedMemberType since we don't know if the solved type is an interface or not. + */ + private enum SuperTypeRelationship { + /** A relationship that has not been determined. */ + UNKNOWN, + /** Indicates that the super type is extended by this type. */ + EXTENDS, + /** Indicates that the super type is implemented by this type. */ + IMPLEMENTS + } + + /** A set of fully qualified names for this type. */ + private Set fullyQualifiedNames = new LinkedHashSet<>(); + + /** + * A flag to ensure that {@link #createAlternatesBasedOnSuperTypeRelationships()} is only called + * once. + */ + private boolean alreadyHandledAllSuperRelationships = false; + + /** A map of super types to their relationships to the type represented by this object. */ + private Map, SuperTypeRelationship> superTypeRelationships = + new LinkedHashMap<>(); + + /** + * Creates a new instance of UnsolvedClassOrInterfaceAlternates. Private constructor; use the + * create methods. + * + * @param potentialDeclaringTypes A list of potential declaring types for this type. + */ + private UnsolvedClassOrInterfaceAlternates( + List potentialDeclaringTypes) { + super(potentialDeclaringTypes); + } + + /** + * Creates a new List of UnsolvedClassOrInterfaceAlternates, given a set of FQNs. The first + * element of the returned list is always the type generated by the FQNs; subsequent classes are + * potential outer classes of the type. The simple class name is the same among all alternates. + * + * @param fqns The set of fqns + * @param generatedSymbolsMap The map of generated symbols from UnsolvedSymbolGenerator. This is + * to ensure potential outer classes are not duplicated + * @return A list of generated types; the first is the type of the fqns, the next few are + * potential outer classes + */ + public static List create( + Set fqns, Map> generatedSymbolsMap) { + if (fqns.isEmpty()) { + throw new RuntimeException("The set of fully-qualified names cannot be empty."); + } + + List allGenerated = new ArrayList<>(); + List potentialDeclaringTypes = new ArrayList<>(); + List alternates = new ArrayList<>(); + + for (String fqn : fqns) { + // In org.example.Class.Class2, we go from Class2 --> Class.Class2 --> example.Class.Class2 + String packageName = fqn.substring(0, fqn.lastIndexOf('.')); + String className = fqn.substring(packageName.length() + 1); + + UnsolvedClassOrInterface type = new UnsolvedClassOrInterface(className, packageName); + alternates.add(type); + + if (packageName.contains(".") && !JavaParserUtil.isProbablyAPackage(packageName)) { + potentialDeclaringTypes.add( + createPotentialContainingClass(packageName, allGenerated, generatedSymbolsMap)); + } + } + + UnsolvedClassOrInterfaceAlternates current = + new UnsolvedClassOrInterfaceAlternates(potentialDeclaringTypes); + + // The first element of the list should always be the type generated by a call + // to this method, for predictable access to consumers of this method. + allGenerated.add(0, current); + + for (UnsolvedClassOrInterface alt : alternates) { + current.addAlternate(alt); + } + + return allGenerated; + } + + /** + * Helper method to create parent classes, based on a FQN. For example, if org.example.Class is + * passed in, Class in org.example is created, while class example in package org is also created. + * + * @param fqn The fully-qualified name + * @param allGenerated A list of all generated symbols (such as org.example.Class and org.example) + * @param generatedSymbolsMap The map of fully-qualified names to generated symbols from + * UnsolvedSymbolGenerator. This is to ensure potential outer classes are not duplicated + * @return The most immediate unsolved type generated; i.e., the one that has an FQN equal to the + * argument corresponding with {@code fqn}. + */ + private static UnsolvedClassOrInterfaceAlternates createPotentialContainingClass( + String fqn, + List allGenerated, + Map> generatedSymbolsMap) { + String qualifier = fqn.substring(0, fqn.lastIndexOf('.')); + String className = fqn.substring(qualifier.length() + 1); + + UnsolvedClassOrInterfaceAlternates generated; + + if (generatedSymbolsMap.containsKey(qualifier)) { + return (UnsolvedClassOrInterfaceAlternates) generatedSymbolsMap.get(qualifier); + } + + if (qualifier.contains(".") && !JavaParserUtil.isProbablyAPackage(qualifier)) { + generated = + new UnsolvedClassOrInterfaceAlternates( + List.of( + createPotentialContainingClass(qualifier, allGenerated, generatedSymbolsMap))); + } else { + generated = new UnsolvedClassOrInterfaceAlternates(List.of()); + } + + generated.addAlternate(new UnsolvedClassOrInterface(className, qualifier)); + + allGenerated.add(generated); + return generated; + } + + /** + * Given an updated set of potential fully-qualified names, this method finds the intersection of + * the two sets and updates the existing set. + * + * @param updated The additional set + */ + public void updateFullyQualifiedNames(Set updated) { + // Update in-place; intersection = removing all elements in the original set + // that isn't found in the updated set + fullyQualifiedNames.retainAll(updated); + getAlternates() + .removeIf(alternate -> !fullyQualifiedNames.contains(alternate.getFullyQualifiedName())); + + String simpleName = JavaParserUtil.getSimpleNameFromQualifiedName(updated.iterator().next()); + getAlternateDeclaringTypes() + .removeIf( + alternateDeclType -> + alternateDeclType.getFullyQualifiedNames().stream() + .anyMatch(name -> !fullyQualifiedNames.contains(name + "." + simpleName))); + } + + @Override + public Set getFullyQualifiedNames() { + return fullyQualifiedNames; + } + + @Override + protected void addAlternate(UnsolvedClassOrInterface alternate) { + super.addAlternate(alternate); + this.fullyQualifiedNames.add(alternate.getFullyQualifiedName()); + } + + /** + * Adds a set of mutually exclusive super type to this class, with an unknown relationship + * (superclass/superinterface). + * + * @param superTypes The mutually exclusive super types + */ + public void addSuperType(Set superTypes) { + if (getType() == UnsolvedClassOrInterfaceType.INTERFACE) { + // If we encounter a super type but we're an interface, make it an interface as well + for (MemberType superType : superTypes) { + if (superType instanceof UnsolvedMemberType unsolvedMemberType + && !unsolvedMemberType.getUnsolvedType().equals(this)) { + unsolvedMemberType.getUnsolvedType().setType(UnsolvedClassOrInterfaceType.INTERFACE); + } + } + } + + UnsolvedClassOrInterfaceType commonType = null; + for (MemberType superType : superTypes) { + if (superType instanceof UnsolvedMemberType unsolvedMemberType + && !unsolvedMemberType.getUnsolvedType().equals(this)) { + UnsolvedClassOrInterfaceType type = unsolvedMemberType.getUnsolvedType().getType(); + if (commonType == null) { + commonType = type; + } + + if (commonType != type) { + commonType = null; + break; + } + } + } + + 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.equals(SolvedMemberType.JAVA_LANG_OBJECT)) { + // If the super type is java.lang.Object, we don't need to add it + continue; + } + + sanitizedSuperTypes.addAll(removeAllWildcardsAndReturnPotentialTypes(superType)); + } + + if (sanitizedSuperTypes.isEmpty()) { + return; + } + + if (commonType == UnsolvedClassOrInterfaceType.CLASS) { + superTypeRelationships.put(sanitizedSuperTypes, SuperTypeRelationship.EXTENDS); + } else if (commonType == UnsolvedClassOrInterfaceType.INTERFACE) { + superTypeRelationships.put(sanitizedSuperTypes, SuperTypeRelationship.IMPLEMENTS); + } else if (superTypeRelationships.get(superTypes) == null) { + superTypeRelationships.put(sanitizedSuperTypes, SuperTypeRelationship.UNKNOWN); + } + } + + /** + * Forces a super class relationship for this type. + * + * @param superClass The super class to force + */ + public void forceSuperClass(MemberType superClass) { + if ((superClass instanceof UnsolvedMemberType unsolved + && unsolved.getUnsolvedType().equals(this)) + || superClass.equals(SolvedMemberType.JAVA_LANG_OBJECT)) { + return; + } + + if (getType() == UnsolvedClassOrInterfaceType.INTERFACE) { + throw new RuntimeException("Cannot force a super class relationship on an interface."); + } + + if (getType() == UnsolvedClassOrInterfaceType.UNKNOWN) { + setType(UnsolvedClassOrInterfaceType.CLASS); + } + + superTypeRelationships.put( + new LinkedHashSet<>(removeAllWildcardsAndReturnPotentialTypes(superClass)), + SuperTypeRelationship.EXTENDS); + } + + /** + * Removes a the set containing only superClass from superTypeRelationships. Right now, this + * method serves no purpose other than placing Exception before Error to generate a checked + * exception as a best-effort result. + * + * @param superClass The super class to remove + */ + public void removeSuperClass(MemberType superClass) { + superTypeRelationships.remove(Set.of(superClass)); + } + + /** + * Forces a super interface relationship for this type. + * + * @param superInterface The super interface to force + */ + public void forceSuperInterface(MemberType superInterface) { + if (superInterface instanceof UnsolvedMemberType unsolved + && unsolved.getUnsolvedType().equals(this)) { + return; + } + + if (superInterface.toString().equals("java.lang.annotation.Annotation")) { + superTypeRelationships.clear(); + setType(UnsolvedClassOrInterfaceType.ANNOTATION); + return; + } + + superTypeRelationships.put( + new LinkedHashSet<>(removeAllWildcardsAndReturnPotentialTypes(superInterface)), + SuperTypeRelationship.IMPLEMENTS); + } + + /** + * 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 + * first element of the list. This is primarily used for generating potential super types, since + * the input type may have a wildcard in its type arguments, but we cannot include a wildcard in + * an extends/implements clause. For example, if we encounter Bar = Foo, we need to make + * sure Foo extends Bar, not Bar, since this is not compilable. + * + * @param memberType The member type to remove wildcards from + * @return The potential super types + */ + private List removeAllWildcardsAndReturnPotentialTypes(MemberType memberType) { + if (memberType.getTypeArguments().isEmpty()) { + return List.of(memberType); + } + + List sanitized = new ArrayList<>(); + sanitized.add(memberType); + + for (int i = 0; i < memberType.getTypeArguments().size(); i++) { + MemberType typeArg = memberType.getTypeArguments().get(i); + + // Preferred type arg for best-effort is the argument that corresponds to the same location + // but this isn't always possible + + String preferredTypeArg = null; + if (i < getTypeVariables().size()) { + preferredTypeArg = getTypeVariables().get(i); + } + + List newSanitized = new ArrayList<>(); + boolean isWildcard = typeArg instanceof WildcardMemberType; + for (MemberType halfSanitized : sanitized) { + // Half sanitized means that all member types up to index i are already handled + List newTypeArgs = new ArrayList<>(halfSanitized.getTypeArguments()); + if (preferredTypeArg != null) { + newTypeArgs.set(i, new SolvedMemberType(preferredTypeArg)); + newSanitized.add(halfSanitized.copyWithNewTypeArgs(newTypeArgs)); + + newTypeArgs = new ArrayList<>(halfSanitized.getTypeArguments()); + } + + if (isWildcard) { + for (String potentialTypeArg : getTypeVariables()) { + if (potentialTypeArg.equals(preferredTypeArg)) { + continue; + } + + newTypeArgs.set(i, new SolvedMemberType(potentialTypeArg)); + newSanitized.add(halfSanitized.copyWithNewTypeArgs(newTypeArgs)); + + newTypeArgs = new ArrayList<>(halfSanitized.getTypeArguments()); + } + } else { + for (MemberType potentialTypeArg : removeAllWildcardsAndReturnPotentialTypes(typeArg)) { + newTypeArgs.set(i, potentialTypeArg); + newSanitized.add(halfSanitized.copyWithNewTypeArgs(newTypeArgs)); + + newTypeArgs = new ArrayList<>(halfSanitized.getTypeArguments()); + } + } + + sanitized = newSanitized; + } + } + + return sanitized; + } + + /** + * This method creates alternate types based on super type relationships collected during unsolved + * symbol generation. This should be the very last step in the process, and should only be called + * once on each UnsolvedClassOrInterfaceAlternates. + */ + public void createAlternatesBasedOnSuperTypeRelationships() { + if (alreadyHandledAllSuperRelationships) { + throw new RuntimeException( + "createAlternatesBasedOnSuperTypeRelationships should only be called once."); + } + + alreadyHandledAllSuperRelationships = true; + + if (superTypeRelationships.isEmpty()) { + // No super types; nothing to do + return; + } + UnsolvedClassOrInterfaceType type = getType(); + + // If the type is unknown, but any super type is a class, then this must also be a class as + // well. + if (type == UnsolvedClassOrInterfaceType.UNKNOWN && isAnySuperTypeAClass(this)) { + type = UnsolvedClassOrInterfaceType.CLASS; + setType(type); + } + + switch (type) { + case INTERFACE -> createAlternatesBasedOnSuperTypeRelationshipsForInterface(getAlternates()); + case CLASS, ENUM -> createAlternatesBasedOnSuperTypeRelationshipsForClass(getAlternates()); + case ANNOTATION -> { + // An annotation cannot extend or implement other types + } + case UNKNOWN -> { + // If the type is unknown, create alternates for both classes and interfaces, and pass + // in those respective lists + + List classAlternates = new ArrayList<>(); + List interfaceAlternates = new ArrayList<>(); + + for (UnsolvedClassOrInterface alternate : getAlternates()) { + UnsolvedClassOrInterface asInterface = alternate.copy(); + alternate.setType(UnsolvedClassOrInterfaceType.CLASS); + asInterface.setType(UnsolvedClassOrInterfaceType.INTERFACE); + + classAlternates.add(alternate); + interfaceAlternates.add(asInterface); + } + + createAlternatesBasedOnSuperTypeRelationshipsForClass(classAlternates); + createAlternatesBasedOnSuperTypeRelationshipsForInterface(interfaceAlternates); + + getAlternates().clear(); + getAlternates().addAll(classAlternates); + getAlternates().addAll(interfaceAlternates); + } + } + } + + /** + * This helper method creates alternates based on super type relationships for interfaces. + * + * @param alternates The initial list of alternates. If {@link #getType()} returns INTERFACE, you + * should pass getAlternates() here. This list will be modified as a side effect, and its + * values may be cleared and overwritten after a call to this method. + */ + private void createAlternatesBasedOnSuperTypeRelationshipsForInterface( + List alternates) { + // Must implement means that all alternates must implement this interface, because + // no superinterfaces also implement this interface. + List mustImplement = new ArrayList<>(); + List> optionalImplement = new ArrayList<>(); + + for (Set superTypeSet : superTypeRelationships.keySet()) { + if (superTypeSet.size() > 1) { + optionalImplement.add(superTypeSet); + continue; + } + + MemberType superType = superTypeSet.iterator().next(); + + boolean isImplementedByAnother = false; + for (Set superTypes2 : superTypeRelationships.keySet()) { + if (!superTypeSet.equals(superTypes2) + && superTypes2.stream() + .allMatch(type -> doesASuperTypeImplementOrExtend(type, superType))) { + isImplementedByAnother = true; + break; + } + } + + if (!isImplementedByAnother) { + mustImplement.add(superType); + } else { + optionalImplement.add(superTypeSet); + } + } + + for (UnsolvedClassOrInterface alternate : alternates) { + alternate.implement(mustImplement); + } + + // If mustImplement is not empty, then it is possible that those interfaces are enough, + // so we'll keep those alternates without adding additional interfaces. However, if it + // is empty, then we should not preserve the alternates without any interfaces. + + List originalAlternates = List.copyOf(alternates); + if (mustImplement.isEmpty()) { + alternates.clear(); + } + + for (List> subset : JavaParserUtil.generateSubsets(optionalImplement)) { + for (List combination : JavaParserUtil.generateAllCombinations(subset)) { + for (UnsolvedClassOrInterface alternate : originalAlternates) { + UnsolvedClassOrInterface copy = alternate.copy(); + copy.implement(combination); + + alternates.add(copy); + } + } + } + } + + /** + * This helper method creates alternates based on super type relationships for classes. + * + * @param alternates The initial list of alternates. If {@link #getType()} returns CLASS/ENUM, you + * should pass getAlternates() here. This list will be modified as a side effect, and its + * values may be cleared and overwritten after a call to this method. + */ + private void createAlternatesBasedOnSuperTypeRelationshipsForClass( + List alternates) { + List originalAlternates = List.copyOf(alternates); + + alternates.clear(); + + List withExtends = new ArrayList<>(); + List> toImplement = new ArrayList<>(); + + for (Entry, SuperTypeRelationship> entry : superTypeRelationships.entrySet()) { + if (entry.getValue() == SuperTypeRelationship.IMPLEMENTS + || entry.getKey().stream() + .allMatch( + type -> + type instanceof UnsolvedMemberType unsolved + && unsolved.getUnsolvedType().getType() + == UnsolvedClassOrInterfaceType.INTERFACE)) { + toImplement.add(entry.getKey()); + continue; + } + + for (UnsolvedClassOrInterface originalAlternate : originalAlternates) { + for (MemberType potentialExtend : entry.getKey()) { + UnsolvedClassOrInterface copy = originalAlternate.copy(); + copy.extend(potentialExtend); + withExtends.add(copy); + } + } + + // If unknown, it could also be an interface + if (entry.getValue() == SuperTypeRelationship.UNKNOWN) { + toImplement.add(entry.getKey()); + } + } + + if (toImplement.isEmpty()) { + alternates.addAll(withExtends); + } else { + for (List combination : JavaParserUtil.generateAllCombinations(toImplement)) { + for (UnsolvedClassOrInterface originalAlternate : + withExtends.isEmpty() ? originalAlternates : withExtends) { + UnsolvedClassOrInterface copy = originalAlternate.copy(); + + for (MemberType interfaceType : combination) { + if (copy.doesExtend(interfaceType)) { + continue; + } + + copy.implement(interfaceType); + } + + alternates.add(copy); + } + } + } + } + + /** + * Recursively checks if a super type implements or extends a target type. For example, if given a + * supertype com.example.Foo and a target of com.example.Bar, this method will return true if + * com.example.Foo or any of its ancestors implement or extend com.example.Bar. + * + * @param superType The super type to check + * @param target The target type to find + * @return True if the super type implements or extends the target type, false otherwise. + */ + private boolean doesASuperTypeImplementOrExtend(MemberType superType, MemberType target) { + if (superType.equals(target)) { + return true; + } + + if (superType instanceof UnsolvedMemberType unsolved) { + UnsolvedClassOrInterfaceAlternates type = unsolved.getUnsolvedType(); + + for (Set superTypeOfSuperType : type.superTypeRelationships.keySet()) { + if (superTypeOfSuperType.stream() + .allMatch(s -> doesASuperTypeImplementOrExtend(s, target))) { + return true; + } + } + } + + return false; + } + + /** + * Recursively checks if any super type is a class. + * + * @param unsolvedAlternates The super type to check + * @return True if any super type is a class. + */ + private boolean isAnySuperTypeAClass(UnsolvedClassOrInterfaceAlternates unsolvedAlternates) { + for (Entry, SuperTypeRelationship> superType : + unsolvedAlternates.superTypeRelationships.entrySet()) { + if (superType.getValue() == SuperTypeRelationship.EXTENDS) { + return true; + } + + boolean allTrue = true; + for (MemberType potentialMemberType : superType.getKey()) { + if (potentialMemberType instanceof UnsolvedMemberType unsolved) { + allTrue &= + unsolved.getUnsolvedType().getType() == UnsolvedClassOrInterfaceType.CLASS + || isAnySuperTypeAClass(unsolved.getUnsolvedType()); + + if (!allTrue) { + break; + } + } + } + + if (allTrue) { + return true; + } + } + + return false; + } + + /** + * Returns true if this class has an extends clause. + * + * @return True if this class has an extends clause. + */ + @Override + public boolean hasExtends() { + return doAllAlternatesReturnTrueFor(UnsolvedClassOrInterface::hasExtends); + } + + /** + * Returns true if this class extends the given extendsType. + * + * @param extendsType The type to extend + * @return True if this type extends the given extendsType. + */ + @Override + public boolean doesExtend(MemberType extendsType) { + return getSuperTypeRelationship(extendsType) == SuperTypeRelationship.EXTENDS; + } + + @Override + public void addAnnotation(String annotation) { + applyToAllAlternates(UnsolvedClassOrInterface::addAnnotation, annotation); + } + + /** + * Returns true if this class implements the given interface. + * + * @param interfaceType The type to implement + * @return True if this type implements the given interface. + */ + @Override + public boolean doesImplement(MemberType interfaceType) { + return getSuperTypeRelationship(interfaceType) == SuperTypeRelationship.IMPLEMENTS; + } + + /** + * Returns true if the given superType is indeed a super type of this current class (does not + * matter if the relationship is extends, implements, or unknown). + * + * @param superType The super type + * @return True if this instance is a child of the given superType + */ + public boolean isAChildOf(MemberType superType) { + return getSuperTypeRelationship(superType) != null; + } + + /** + * Checks to see if the given superType is in any mutually exclusive set key of the + * superTypeRelationships map. + * + * @param superType The super type to check + * @return The relationship if found, null otherwise + */ + private @Nullable SuperTypeRelationship getSuperTypeRelationship(MemberType superType) { + SuperTypeRelationship result = superTypeRelationships.get(Set.of(superType)); + + if (result != null) { + return result; + } + + for (Set mutuallyExclusiveSet : superTypeRelationships.keySet()) { + if (mutuallyExclusiveSet.contains(superType)) { + return superTypeRelationships.get(mutuallyExclusiveSet); + } + } + + return null; + } + + @Override + public void setTypeVariables(int number) { + applyToAllAlternates(UnsolvedClassOrInterface::setTypeVariables, number); + } + + /** + * Gets the type variables as a list + * + * @return The type variables as a list + */ + @Override + public List getTypeVariables() { + return getAlternates().get(0).getTypeVariables(); + } + + @Override + public @ClassGetSimpleName String getClassName() { + return getAlternates().get(0).getClassName(); + } + + @Override + public UnsolvedClassOrInterfaceType getType() { + UnsolvedClassOrInterfaceType typeOfLast = null; + // All the types will be the same if we know with certainty what type of type it is. If + // there are multiple different types, then the overall type of this type is unknown. + for (UnsolvedClassOrInterface alternate : getAlternates()) { + if (typeOfLast == null) { + typeOfLast = alternate.getType(); + continue; + } + + if (alternate.getType() != typeOfLast) { + return UnsolvedClassOrInterfaceType.UNKNOWN; + } + } + + if (typeOfLast == null) { + throw new RuntimeException( + "Cannot have an UnsolvedClassOrInterfaceAlternates without any alternates."); + } + + return typeOfLast; + } + + @Override + public void setType(UnsolvedClassOrInterfaceType type) { + UnsolvedClassOrInterfaceType originalType = getType(); + + if (originalType == type) { + return; + } + + boolean areAllAlternatesOfSameType = + doAllAlternatesReturnTrueFor((alt) -> alt.getType() == type); + + applyToAllAlternates(UnsolvedClassOrInterface::setType, type); + + // If all alternates were already the same type, we don't need to remove duplicates + if (areAllAlternatesOfSameType) { + removeDuplicateAlternates(); + } + + if (type == UnsolvedClassOrInterfaceType.INTERFACE) { + // All supertypes must also be interfaces + for (MemberType superType : + superTypeRelationships.keySet().stream().flatMap(Set::stream).toList()) { + if (superType instanceof UnsolvedMemberType unsolved) { + unsolved.getUnsolvedType().setType(UnsolvedClassOrInterfaceType.INTERFACE); + } + } + } + + if (originalType != UnsolvedClassOrInterfaceType.ANNOTATION + && type == UnsolvedClassOrInterfaceType.ANNOTATION) { + for (UnsolvedClassOrInterface alternate : List.copyOf(getAlternates())) { + UnsolvedClassOrInterface typeUseAnnos = alternate.copy(); + UnsolvedClassOrInterface typeAnnos = alternate.copy(); + alternate.addAnnotation( + "@java.lang.annotation.Target({ java.lang.annotation.ElementType.TYPE_USE," + + " java.lang.annotation.ElementType.TYPE, java.lang.annotation.ElementType.FIELD," + + " java.lang.annotation.ElementType.METHOD," + + " java.lang.annotation.ElementType.PARAMETER," + + " java.lang.annotation.ElementType.CONSTRUCTOR," + + " java.lang.annotation.ElementType.LOCAL_VARIABLE," + + " java.lang.annotation.ElementType.ANNOTATION_TYPE," + + " java.lang.annotation.ElementType.PACKAGE," + + " java.lang.annotation.ElementType.TYPE_PARAMETER})"); + typeUseAnnos.addAnnotation( + "@java.lang.annotation.Target(java.lang.annotation.ElementType.TYPE_USE)"); + typeAnnos.addAnnotation( + "@java.lang.annotation.Target({ java.lang.annotation.ElementType.TYPE," + + " java.lang.annotation.ElementType.FIELD," + + " java.lang.annotation.ElementType.METHOD," + + " java.lang.annotation.ElementType.PARAMETER," + + " java.lang.annotation.ElementType.CONSTRUCTOR," + + " java.lang.annotation.ElementType.LOCAL_VARIABLE," + + " java.lang.annotation.ElementType.ANNOTATION_TYPE," + + " java.lang.annotation.ElementType.PACKAGE," + + " java.lang.annotation.ElementType.TYPE_PARAMETER})"); + + addAlternate(typeUseAnnos); + addAlternate(typeAnnos); + } + } + } + + @Override + public void setTypeVariables(List typeVariables) { + applyToAllAlternates(UnsolvedClassOrInterface::setTypeVariables, typeVariables); + } +} diff --git a/src/main/java/org/checkerframework/specimin/unsolved/UnsolvedClassOrInterfaceCommon.java b/src/main/java/org/checkerframework/specimin/unsolved/UnsolvedClassOrInterfaceCommon.java new file mode 100644 index 000000000..fd20e95bc --- /dev/null +++ b/src/main/java/org/checkerframework/specimin/unsolved/UnsolvedClassOrInterfaceCommon.java @@ -0,0 +1,84 @@ +package org.checkerframework.specimin.unsolved; + +import java.util.List; +import org.checkerframework.checker.signature.qual.ClassGetSimpleName; + +/** + * Common interface for {@link UnsolvedClassOrInterface} and {@link + * UnsolvedClassOrInterfaceAlternates}. Each getter should return the same value for each alternate; + * each setter should do the same operation to each alternate. If these requirements are not met, do + * not include the method in this interface. + */ +public interface UnsolvedClassOrInterfaceCommon { + /** + * Returns the type of this type. i.e., is it a class, interface, annotation, or enum? + * + * @return The type of this type + */ + public UnsolvedClassOrInterfaceType getType(); + + /** + * Sets the type of this type. + * + * @param type The type to set this type to + */ + public void setType(UnsolvedClassOrInterfaceType type); + + /** + * Returns true if this class has an extends clause. + * + * @return True if this class has an extends clause. + */ + public boolean hasExtends(); + + /** + * Returns true if any alternate extends the given extendsType. + * + * @param extendsType The superclass + * @return True if any alternate extends the given extendsType. + */ + public boolean doesExtend(MemberType extendsType); + + /** + * Adds an annotation to this class. + * + * @param annotation a fully-qualified annotation to apply + */ + public void addAnnotation(String annotation); + + /** + * Returns true if any alternate implements the given interface. + * + * @param interfaceType The type of the interface + * @return True if this type implements the given interface. + */ + public boolean doesImplement(MemberType interfaceType); + + /** + * Autogenerates the given amount of type variables (T, T1, T2, ...) + * + * @param number The number of type variables. + */ + public void setTypeVariables(int number); + + /** + * Sets the preferred type variables. + * + * @param preferredTypeVariables The preferred type variables. + */ + public void setTypeVariables(List preferredTypeVariables); + + /** + * Gets the type variables as a list. + * + * @return The type variables as a list + */ + public List getTypeVariables(); + + /** + * Get the name of this class (note: without any generic type variables). + * + * @return the name of the class + */ + public @ClassGetSimpleName String getClassName(); +} diff --git a/src/main/java/org/checkerframework/specimin/unsolved/UnsolvedClassOrInterfaceType.java b/src/main/java/org/checkerframework/specimin/unsolved/UnsolvedClassOrInterfaceType.java new file mode 100644 index 000000000..61e872ce2 --- /dev/null +++ b/src/main/java/org/checkerframework/specimin/unsolved/UnsolvedClassOrInterfaceType.java @@ -0,0 +1,15 @@ +package org.checkerframework.specimin.unsolved; + +/** Represents the type that UnsolvedClassOrInterface represents. */ +public enum UnsolvedClassOrInterfaceType { + /** Represents an unknown type; could be any value in this enum. */ + UNKNOWN, + /** Represents a class type. */ + CLASS, + /** Represents an interface type. */ + INTERFACE, + /** Represents an annotation type. */ + ANNOTATION, + /** Represents an enum type. */ + ENUM +} diff --git a/src/main/java/org/checkerframework/specimin/unsolved/UnsolvedField.java b/src/main/java/org/checkerframework/specimin/unsolved/UnsolvedField.java new file mode 100644 index 000000000..dfff3da28 --- /dev/null +++ b/src/main/java/org/checkerframework/specimin/unsolved/UnsolvedField.java @@ -0,0 +1,120 @@ +package org.checkerframework.specimin.unsolved; + +import com.github.javaparser.ast.Node; +import java.util.Objects; +import java.util.Set; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.specimin.JavaParserUtil; + +/** Represents a single unsolved field alternate. */ +public class UnsolvedField extends UnsolvedSymbolAlternate implements UnsolvedFieldCommon { + /** The name of the field */ + private final String name; + + /** The type of the field. */ + private MemberType type; + + /** This is set to true if this field is a static field */ + private boolean isStatic = false; + + /** This is set to true if this field is a final field */ + private boolean isFinal = false; + + /** + * Create an instance of UnsolvedField. + * + * @param name the name of the field + * @param type the type of the field + * @param isStatic if the field is static + * @param isFinal if the field is final + * @param mustPreserveNodes the nodes that must be preserved + */ + public UnsolvedField( + String name, + MemberType type, + boolean isStatic, + boolean isFinal, + Set mustPreserveNodes) { + super(mustPreserveNodes); + this.name = name; + this.type = type; + this.isStatic = isStatic; + this.isFinal = isFinal; + } + + /** + * Get the type of this field + * + * @return the value of type + */ + public MemberType getType() { + return type; + } + + /** + * Sets the type of this field. + * + * @param type the new type to set + */ + public void setType(MemberType type) { + this.type = type; + } + + /** + * Get the name of this field + * + * @return the name of this field + */ + @Override + public String getName() { + return name; + } + + /** + * Return the content of this field. If declaringTypeType is ENUM, then this is also formatted as + * an enum constant declaration. + * + * @param declaringTypeType The type of the declaring type + * @return The field declaration + */ + public String toString(UnsolvedClassOrInterfaceType declaringTypeType) { + if (declaringTypeType == UnsolvedClassOrInterfaceType.ENUM) { + // Still compilable even with an extra comma at the end + return name + ","; + } + return "public " + + (isStatic ? "static " : "") + + (isFinal ? "final " : "") + + type + + " " + + name + + (isStatic && isFinal ? " = " + JavaParserUtil.getInitializerRHS(type.toString()) : "") + + ";"; + } + + @Override + public boolean equals(@Nullable Object o) { + if (!(o instanceof UnsolvedField other)) { + return false; + } + return other.name.equals(this.name) + && other.type.equals(this.type) + && other.isStatic == this.isStatic + && other.isFinal == this.isFinal; + } + + @Override + public int hashCode() { + return Objects.hash(name, type, isStatic, isFinal); + } + + @Override + public boolean isStatic() { + return isStatic; + } + + @Override + public boolean isFinal() { + return isFinal; + } +} diff --git a/src/main/java/org/checkerframework/specimin/unsolved/UnsolvedFieldAlternates.java b/src/main/java/org/checkerframework/specimin/unsolved/UnsolvedFieldAlternates.java new file mode 100644 index 000000000..f102c7f32 --- /dev/null +++ b/src/main/java/org/checkerframework/specimin/unsolved/UnsolvedFieldAlternates.java @@ -0,0 +1,208 @@ +package org.checkerframework.specimin.unsolved; + +import com.github.javaparser.ast.body.CallableDeclaration; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * Given a name, field type, and a set of potential encapsulating classes, this class allows for + * alternates of a same field to be generated in different locations. If a class were: + * + *

+ * class A extends B implements C {
+ *    void x() {
+ *      int y = a;
+ *    }
+ * }
+ * 
+ * + * where B and C are both unresolvable, field a could be in either one. + */ +public class UnsolvedFieldAlternates extends UnsolvedSymbolAlternates + implements UnsolvedFieldCommon { + + /** + * Creates a new instance of UnsolvedFieldAlternates. Private constructor; use the create methods. + * + * @param alternateDeclaringTypes A list of potential declaring types for this field. + */ + private UnsolvedFieldAlternates( + List alternateDeclaringTypes) { + super(alternateDeclaringTypes); + } + + /** + * Creates a new instance of a field. Note that there is only one alternate generated here, but + * there could potentially be many different declaring types. + * + * @param name The name of the field + * @param typesToMustPreserveNodes A map of field types to must-preserve nodes. Different field + * types may lead to different sets of nodes that need to be conditionally preserved. + * @param alternateDeclaringTypes Potential declaring types + * @param isStatic Whether the field is static or not + * @param isFinal Whether the field is final or not + * @return The generated field + */ + public static UnsolvedFieldAlternates create( + String name, + Map> typesToMustPreserveNodes, + List alternateDeclaringTypes, + boolean isStatic, + boolean isFinal) { + if (alternateDeclaringTypes.isEmpty()) { + throw new RuntimeException("Unsolved field must have at least one potential declaring type."); + } + + UnsolvedFieldAlternates result = new UnsolvedFieldAlternates(alternateDeclaringTypes); + + for (Map.Entry> entry : + typesToMustPreserveNodes.entrySet()) { + UnsolvedField field = + new UnsolvedField(name, entry.getKey(), isStatic, isFinal, Set.of(entry.getValue())); + result.addAlternate(field); + } + + return result; + } + + /** + * Creates a new instance of a field. Note that there is only one alternate generated here, but + * there could potentially be many different declaring types. + * + * @param name The name of the field + * @param type The type of the field + * @param alternateDeclaringTypes Potential declaring types + * @param isStatic Whether the field is static or not + * @param isFinal Whether the field is final or not + * @return The generated field + */ + public static UnsolvedFieldAlternates create( + String name, + MemberType type, + List alternateDeclaringTypes, + boolean isStatic, + boolean isFinal) { + return create(name, Set.of(type), alternateDeclaringTypes, isStatic, isFinal); + } + + /** + * Creates a new instance of a field. Note that there is only one alternate generated here, but + * there could potentially be many different declaring types. + * + * @param name The name of the field + * @param types The potential types of the field + * @param alternateDeclaringTypes Potential declaring types + * @param isStatic Whether the field is static or not + * @param isFinal Whether the field is final or not + * @return The generated field + */ + public static UnsolvedFieldAlternates create( + String name, + Set types, + List alternateDeclaringTypes, + boolean isStatic, + boolean isFinal) { + if (alternateDeclaringTypes.isEmpty()) { + throw new RuntimeException("Unsolved field must have at least one potential declaring type."); + } + + if (types.isEmpty()) { + throw new RuntimeException("Unsolved field must have at least one potential type."); + } + + UnsolvedFieldAlternates result = new UnsolvedFieldAlternates(alternateDeclaringTypes); + + for (MemberType type : types) { + UnsolvedField field = new UnsolvedField(name, type, isStatic, isFinal, Set.of()); + result.addAlternate(field); + } + + return result; + } + + /** + * Updates field types and must preserve nodes. Saves the intersection of the previous and the + * input, since we know more information to narrow potential field types down. + * + * @param typesToPreserveNodes A map of field types to nodes that must be preserved + */ + public void updateFieldTypesAndMustPreserveNodes( + Map> typesToPreserveNodes) { + // Update in-place; intersection = removing all elements in the original set + // that isn't found in the updated set + UnsolvedField old = getAlternates().get(0); + Set oldFieldTypes = getTypes(); + getAlternates().removeIf(alternate -> !typesToPreserveNodes.containsKey(alternate.getType())); + + if (getAlternates().isEmpty() && oldFieldTypes.size() == 1) { + // If it's now empty and old field types was of size 1, it was probably a synthetic field + // type + for (Map.Entry> entry : typesToPreserveNodes.entrySet()) { + UnsolvedField field = + new UnsolvedField( + old.getName(), + entry.getKey(), + old.isStatic(), + old.isFinal(), + Set.of(entry.getValue())); + addAlternate(field); + } + } + } + + @Override + public Set getFullyQualifiedNames() { + Set fqns = new LinkedHashSet<>(); + + for (UnsolvedClassOrInterfaceAlternates alternate : getAlternateDeclaringTypes()) { + for (String fqn : alternate.getFullyQualifiedNames()) { + fqns.add(fqn + "#" + getAlternates().get(0).getName()); + } + } + + return fqns; + } + + /** + * Gets the field types. + * + * @return The field types + */ + public Set getTypes() { + return getAlternates().stream() + .map(alternate -> alternate.getType()) + .collect(Collectors.toCollection(LinkedHashSet::new)); + } + + /** + * Replaces a field type with a new type in all alternates. + * + * @param oldType The type to replace + * @param newType The type to replace with + */ + public void replaceFieldType(MemberType oldType, MemberType newType) { + for (UnsolvedField alternate : getAlternates()) { + if (alternate.getType().equals(oldType)) { + alternate.setType(newType); + } + } + } + + @Override + public String getName() { + return getAlternates().get(0).getName(); + } + + @Override + public boolean isStatic() { + return doAllAlternatesReturnTrueFor(UnsolvedField::isStatic); + } + + @Override + public boolean isFinal() { + return doAllAlternatesReturnTrueFor(UnsolvedField::isFinal); + } +} diff --git a/src/main/java/org/checkerframework/specimin/unsolved/UnsolvedFieldCommon.java b/src/main/java/org/checkerframework/specimin/unsolved/UnsolvedFieldCommon.java new file mode 100644 index 000000000..cf192ad94 --- /dev/null +++ b/src/main/java/org/checkerframework/specimin/unsolved/UnsolvedFieldCommon.java @@ -0,0 +1,29 @@ +package org.checkerframework.specimin.unsolved; + +/** + * Common interface for {@link UnsolvedField} and {@link UnsolvedFieldAlternates}. Each getter + * should return the same value for each alternate; each setter should do the same operation to each + * alternate. If these requirements are not met, do not include the method in this interface. + */ +public interface UnsolvedFieldCommon { + /** + * Get the name of this field + * + * @return the name of this field + */ + public String getName(); + + /** + * Check if this field is static + * + * @return true if this field is static, false otherwise + */ + public boolean isStatic(); + + /** + * Check if this field is final + * + * @return true if this field is final, false otherwise + */ + public boolean isFinal(); +} diff --git a/src/main/java/org/checkerframework/specimin/unsolved/UnsolvedGenerationResult.java b/src/main/java/org/checkerframework/specimin/unsolved/UnsolvedGenerationResult.java new file mode 100644 index 000000000..0944f87b6 --- /dev/null +++ b/src/main/java/org/checkerframework/specimin/unsolved/UnsolvedGenerationResult.java @@ -0,0 +1,13 @@ +package org.checkerframework.specimin.unsolved; + +import java.util.List; + +/** + * Once an operation in {@link UnsolvedSymbolGenerator} is done, an instance of this record is + * returned to represent what needs to be added to the slice and what needs to be removed. + */ +public record UnsolvedGenerationResult( + List> toAdd, List> toRemove) { + /** Represents a result where no symbols need to be added, and none need to be removed. */ + public static UnsolvedGenerationResult EMPTY = new UnsolvedGenerationResult(List.of(), List.of()); +} diff --git a/src/main/java/org/checkerframework/specimin/unsolved/UnsolvedMemberType.java b/src/main/java/org/checkerframework/specimin/unsolved/UnsolvedMemberType.java new file mode 100644 index 000000000..2511f19a6 --- /dev/null +++ b/src/main/java/org/checkerframework/specimin/unsolved/UnsolvedMemberType.java @@ -0,0 +1,115 @@ +package org.checkerframework.specimin.unsolved; + +import java.util.List; +import java.util.Objects; +import java.util.Set; +import org.checkerframework.checker.nullness.qual.Nullable; + +/** + * Represents a {@link MemberType} of an unsolved symbol, wrapping around an {@link + * UnsolvedClassOrInterfaceAlternates}. + * + *

See {@link MemberType} for more details. + */ +public class UnsolvedMemberType extends MemberType { + /** The unsolved class or interface alternates that this member type wraps. */ + private final UnsolvedClassOrInterfaceAlternates unsolved; + + /** The number of array brackets (e.g., 2 for int[][]). */ + private int numArrayBrackets; + + /** + * Creates a new UnsolvedMemberType with the given unsolved type and no array brackets. + * + * @param unsolved The unsolved type + */ + public UnsolvedMemberType(UnsolvedClassOrInterfaceAlternates unsolved) { + this(unsolved, 0); + } + + /** + * Creates a new UnsolvedMemberType with the given unsolved type and number of array brackets. + * + * @param unsolved The unsolved type + * @param numArrayBrackets The number of array brackets + */ + public UnsolvedMemberType(UnsolvedClassOrInterfaceAlternates unsolved, int numArrayBrackets) { + this(unsolved, numArrayBrackets, List.of()); + } + + /** + * Creates a new UnsolvedMemberType with the given unsolved type, number of array brackets, and + * type arguments. + * + * @param unsolved The unsolved type + * @param numArrayBrackets The number of array brackets + * @param typeArguments The type arguments for this type + */ + public UnsolvedMemberType( + UnsolvedClassOrInterfaceAlternates unsolved, + int numArrayBrackets, + List typeArguments) { + super(typeArguments); + this.unsolved = unsolved; + this.numArrayBrackets = numArrayBrackets; + } + + /** + * Gets the unsolved type that this member type represents. + * + * @return The unsolved type + */ + public UnsolvedClassOrInterfaceAlternates getUnsolvedType() { + return unsolved; + } + + @Override + public Set getFullyQualifiedNames() { + return unsolved.getFullyQualifiedNames(); + } + + @Override + public String toString() { + // TODO: handle more than one alternate + StringBuilder sb = new StringBuilder(); + + sb.append(getFullyQualifiedNames().iterator().next()); + + if (!getTypeArguments().isEmpty()) { + sb.append('<'); + for (int i = 0; i < getTypeArguments().size(); i++) { + if (i > 0) { + sb.append(", "); + } + sb.append(getTypeArguments().get(i).toString()); + } + sb.append('>'); + } + + if (numArrayBrackets > 0) { + sb.append("[]".repeat(numArrayBrackets)); + } + + return sb.toString(); + } + + @Override + public boolean equals(@Nullable Object other) { + if (!(other instanceof UnsolvedMemberType otherAsUnsolvedMemberType)) { + return false; + } + + return Objects.equals(otherAsUnsolvedMemberType.unsolved, this.unsolved) + && Objects.equals(otherAsUnsolvedMemberType.getTypeArguments(), this.getTypeArguments()); + } + + @Override + public int hashCode() { + return Objects.hash(unsolved, getTypeArguments()); + } + + @Override + public MemberType copyWithNewTypeArgs(List newTypeArgs) { + return new UnsolvedMemberType(unsolved, numArrayBrackets, newTypeArgs); + } +} diff --git a/src/main/java/org/checkerframework/specimin/unsolved/UnsolvedMethod.java b/src/main/java/org/checkerframework/specimin/unsolved/UnsolvedMethod.java new file mode 100644 index 000000000..68c2decf3 --- /dev/null +++ b/src/main/java/org/checkerframework/specimin/unsolved/UnsolvedMethod.java @@ -0,0 +1,299 @@ +package org.checkerframework.specimin.unsolved; + +import com.github.javaparser.ast.Node; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import org.checkerframework.checker.nullness.qual.Nullable; + +/** + * An UnsolvedMethod instance is a representation of a method that can not be solved by + * SymbolSolver. The reason is that the class file of that method is not in the root directory. + * + *

Note for {@link #equals}: Use with caution: two UnsolvedMethods may return not equal + * but they may belong to the same UnsolvedMethodAlternates. This could be the case when the same + * unsolved method is called but there are multiple possibilities for a parameter type. When able + * to, call .equals on UnsolvedMethodAlternates instead of here. + */ +public class UnsolvedMethod extends UnsolvedSymbolAlternate implements UnsolvedMethodCommon { + /** The name of the method */ + private final String name; + + /** The return type of the method. */ + private MemberType returnType; + + /** The list of the types of the parameters of the method. */ + private final List parameterList; + + /** This field is set to true if this method is a static method */ + private boolean isStatic = false; + + /** The list of the types of the exceptions thrown by the method. */ + private final List throwsList; + + /** The number of type variables for this method. */ + private int numberOfTypeVariables = 0; + + /** The access modifier of the method. */ + private String accessModifier; + + /** + * Create an instance of UnsolvedMethod. + * + * @param name the name of the method + * @param returnType the return type of the method + * @param parameterList the list of parameters for this method + * @param throwsList the list of exceptions thrown by this method + * @param mustPreserve the set of nodes that must be preserved with this alternate + */ + public UnsolvedMethod( + String name, + MemberType returnType, + List parameterList, + List throwsList, + Set mustPreserve) { + this(name, returnType, parameterList, throwsList, mustPreserve, "public"); + } + + /** + * Create an instance of UnsolvedMethod. + * + * @param name the name of the method + * @param returnType the return type of the method + * @param parameterList the list of parameters for this method + * @param throwsList the list of exceptions thrown by this method + * @param accessModifier the access modifier of this method + * @param mustPreserve the set of nodes that must be preserved with this alternate + */ + public UnsolvedMethod( + String name, + MemberType returnType, + List parameterList, + List throwsList, + Set mustPreserve, + String accessModifier) { + super(mustPreserve); + this.name = name; + this.returnType = returnType; + // Parameter list should be mutable, so convert it to be safe + this.parameterList = new ArrayList<>(parameterList); + this.throwsList = throwsList; + this.accessModifier = accessModifier; + } + + /** + * Get the return type of this method. + * + * @return the value of returnType + */ + public MemberType getReturnType() { + return returnType; + } + + /** + * Get the name of this method. + * + * @return the name of this method + */ + @Override + public String getName() { + return name; + } + + /** + * Getter for the parameter list. Note that the list is read-only. + * + * @return the parameter list + */ + public List getParameterList() { + return Collections.unmodifiableList(parameterList); + } + + /** + * Replaces the type of a parameter in the parameter list with a new type. + * + * @param oldType The old type + * @param newType The new type + */ + public void replaceParameterType(MemberType oldType, MemberType newType) { + for (int i = 0; i < parameterList.size(); i++) { + if (parameterList.get(i).equals(oldType)) { + parameterList.set(i, newType); + } + } + } + + /** + * Getter for the throws list. Note that the list is read-only. + * + * @return the throws list + */ + @Override + public List getThrownExceptions() { + return Collections.unmodifiableList(throwsList); + } + + /** Set isStatic to true */ + @Override + public void setStatic() { + isStatic = true; + } + + /** + * This method sets the number of type variables for the current class + * + * @param numberOfTypeVariables number of type variable in this class. + */ + @Override + public void setNumberOfTypeVariables(int numberOfTypeVariables) { + this.numberOfTypeVariables = numberOfTypeVariables; + } + + @Override + public void setReturnType(MemberType returnType) { + this.returnType = returnType; + } + + /** + * Use with caution: two UnsolvedMethods may return not equal here but they may belong to + * the same UnsolvedMethodAlternates. This could be the case when the same unsolved method is + * called but there are multiple possibilities for a parameter type. When able to, call .equals on + * UnsolvedMethodAlternates instead of here. + * + *

{@inheritDoc} + */ + @Override + public boolean equals(@Nullable Object o) { + if (!(o instanceof UnsolvedMethod)) { + return false; + } + UnsolvedMethod other = (UnsolvedMethod) o; + return other.name.equals(this.name) + && other.parameterList.equals(parameterList) + && other.returnType.equals(this.returnType); + } + + @Override + public int hashCode() { + return Objects.hash(name, parameterList, returnType); + } + + /** + * Return the content of the method. Note that the body of the method is stubbed out. + * + * @param type The type of the declaring type + * @return the content of the method with the body stubbed out + */ + public String toString(UnsolvedClassOrInterfaceType type) { + StringBuilder arguments = new StringBuilder(); + for (int i = 0; i < parameterList.size(); i++) { + MemberType parameterType = parameterList.get(i); + + arguments.append(parameterType).append(" ").append("parameter").append(i); + if (i < parameterList.size() - 1) { + arguments.append(", "); + } + } + StringBuilder signature = new StringBuilder(); + if (accessModifier != null || accessModifier.isEmpty()) { + signature.append(accessModifier); + signature.append(" "); + } + + if (isStatic) { + signature.append("static "); + } + + String typeVariables = getTypeVariablesAsString(); + + if (!typeVariables.equals("")) { + signature.append(getTypeVariablesAsString()).append(" "); + } + + String returnTypeAsString = returnType.toString(); + if (!"".equals(returnTypeAsString)) { + signature.append(returnTypeAsString).append(" "); + } + signature.append(name).append("("); + signature.append(arguments); + signature.append(")"); + + if (throwsList.size() > 0) { + signature.append(" throws "); + } + + StringBuilder exceptions = new StringBuilder(); + for (int i = 0; i < throwsList.size(); i++) { + MemberType exception = throwsList.get(i); + exceptions.append(exception); + if (i < throwsList.size() - 1) { + exceptions.append(", "); + } + } + signature.append(exceptions); + + if (type == UnsolvedClassOrInterfaceType.ANNOTATION + || type == UnsolvedClassOrInterfaceType.INTERFACE) { + return "\n " + signature + ";\n"; + } else { + return "\n " + signature + " {\n throw new java.lang.Error();\n }\n"; + } + } + + /** + * Gets the number of type variables. + * + * @return The number of type variables + */ + @Override + public int getNumberOfTypeVariables() { + return numberOfTypeVariables; + } + + /** + * Return a synthetic representation for type variables of the current class. + * + * @return the synthetic representation for type variables + */ + private String getTypeVariablesAsString() { + if (numberOfTypeVariables == 0) { + return ""; + } + StringBuilder result = new StringBuilder(); + // if class A has three type variables, the expression will be A + result.append("<"); + getTypeVariablesImpl(result); + result.append(">"); + return result.toString(); + } + + /** + * Helper method for {@link #getTypeVariablesAsString()}. + * + * @param result a string builder. Will be side-effected. + */ + private void getTypeVariablesImpl(StringBuilder result) { + for (int i = 0; i < numberOfTypeVariables; i++) { + String typeExpression = "T" + ((i > 0) ? i : ""); + result.append(typeExpression).append(", "); + } + result.delete(result.length() - 2, result.length()); + } + + @Override + public String getAccessModifier() { + return accessModifier; + } + + @Override + public void setAccessModifier(String accessModifier) { + this.accessModifier = accessModifier; + } + + @Override + public boolean isStatic() { + return isStatic; + } +} diff --git a/src/main/java/org/checkerframework/specimin/unsolved/UnsolvedMethodAlternates.java b/src/main/java/org/checkerframework/specimin/unsolved/UnsolvedMethodAlternates.java new file mode 100644 index 000000000..cb65cb942 --- /dev/null +++ b/src/main/java/org/checkerframework/specimin/unsolved/UnsolvedMethodAlternates.java @@ -0,0 +1,376 @@ +package org.checkerframework.specimin.unsolved; + +import com.github.javaparser.ast.Node; +import com.github.javaparser.ast.body.CallableDeclaration; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.specimin.JavaParserUtil; + +/** + * /** Given a name, return type, a set of parameters and a set of potential encapsulating classes, + * this class allows for alternates of a same field to be generated in different locations. If a + * class were: + * + *


+ * class A extends B implements C {
+ *    void x() {
+ *      int y = a();
+ *    }
+ * }
+ * 
+ * + * where B and C are both unresolvable, method a() could be in either one. + */ +public class UnsolvedMethodAlternates extends UnsolvedSymbolAlternates + implements UnsolvedMethodCommon { + /** + * Creates a new instance of UnsolvedMethodAlternates. Private constructor; use the create + * methods. + * + * @param alternateDeclaringTypes A list of potential declaring types for this method. + */ + private UnsolvedMethodAlternates( + List alternateDeclaringTypes) { + super(alternateDeclaringTypes); + } + + /** + * Creates a new unsolved method declaration + * + * @param name The name of the method + * @param types The return types of the method + * @param alternateDeclaringTypes Potential declaring types of the method + * @param parameters Potential parameters of the method. Each set represents a possibility of + * parameter types at that position + * @return The method definition + */ + public static UnsolvedMethodAlternates create( + String name, + Set types, + List alternateDeclaringTypes, + List> parameters) { + return create(name, types, alternateDeclaringTypes, parameters, List.of()); + } + + /** + * Creates a new unsolved method declaration + * + * @param name The name of the method + * @param types The return types of the method + * @param alternateDeclaringTypes Potential declaring types of the method + * @param parameters Potential parameters of the method. Each set represents a possibility of + * parameter types at that position + * @param exceptions Thrown exceptions of this method + * @return The method definition + */ + public static UnsolvedMethodAlternates create( + String name, + Set types, + List alternateDeclaringTypes, + List> parameters, + List exceptions) { + return create(name, types, alternateDeclaringTypes, parameters, exceptions, "public"); + } + + /** + * Creates a new unsolved method declaration + * + * @param name The name of the method + * @param types The return types of the method + * @param alternateDeclaringTypes Potential declaring types of the method + * @param parameters Potential parameters of the method. Each set represents a possibility of + * parameter types at that position + * @param exceptions Thrown exceptions of this method + * @param accessModifier The access modifier of this method + * @return The method definition + */ + public static UnsolvedMethodAlternates create( + String name, + Set types, + List alternateDeclaringTypes, + List> parameters, + List exceptions, + String accessModifier) { + if (alternateDeclaringTypes.isEmpty()) { + throw new RuntimeException( + "Unsolved method must have at least one potential declaring type."); + } + + if (types.isEmpty()) { + throw new RuntimeException("Unsolved method must have at least one potential return type."); + } + + UnsolvedMethodAlternates result = new UnsolvedMethodAlternates(alternateDeclaringTypes); + + for (List parameterList : JavaParserUtil.generateAllCombinations(parameters)) { + for (MemberType type : types) { + UnsolvedMethod method = + new UnsolvedMethod(name, type, parameterList, exceptions, Set.of(), accessModifier); + result.addAlternate(method); + } + } + + return result; + } + + /** + * Creates a new unsolved method declaration + * + * @param name The name of the method + * @param types The return types of the method + * @param alternateDeclaringTypes Potential declaring types of the method + * @param parameters Potential parameters of the method. Each map represents a possibility of + * parameter types at that position, along with nodes that must be preserved if that type is + * chosen + * @param exceptions Thrown exceptions of this method + * @return The method definition + */ + public static UnsolvedMethodAlternates createWithPreservation( + String name, + Set types, + List alternateDeclaringTypes, + List> parameters, + List exceptions) { + if (alternateDeclaringTypes.isEmpty()) { + throw new RuntimeException( + "Unsolved method must have at least one potential declaring type."); + } + + if (types.isEmpty()) { + throw new RuntimeException("Unsolved method must have at least one potential return type."); + } + + UnsolvedMethodAlternates result = new UnsolvedMethodAlternates(alternateDeclaringTypes); + + for (List> parameterList : + JavaParserUtil.generateAllCombinationsForListOfMaps(parameters)) { + for (MemberType type : types) { + List params = parameterList.stream().map(Map.Entry::getKey).toList(); + Set toPreserve = new HashSet<>(); + + for (Map.Entry entry : parameterList) { + Node node = entry.getValue(); + if (node != null) { + toPreserve.add(node); + } + } + + UnsolvedMethod method = new UnsolvedMethod(name, type, params, exceptions, toPreserve); + result.addAlternate(method); + } + } + + return result; + } + + /** + * Creates a new unsolved method declaration + * + * @param name The name of the method + * @param returnTypesToMustPreserveNodes A map of return types to must-preserve nodes. Different + * return types may lead to different sets of nodes that need to be conditionally preserved. + * @param alternateDeclaringTypes Potential declaring types of the method + * @param parameters Potential parameters of the method. Each map represents a possibility of + * parameter types at that position, along with nodes that must be preserved if that type is + * chosen + * @param exceptions Thrown exceptions of this method + * @return The method definition + */ + public static UnsolvedMethodAlternates createWithPreservation( + String name, + Map> returnTypesToMustPreserveNodes, + List alternateDeclaringTypes, + List> parameters, + List exceptions) { + if (alternateDeclaringTypes.isEmpty()) { + throw new RuntimeException( + "Unsolved method must have at least one potential declaring type."); + } + UnsolvedMethodAlternates result = new UnsolvedMethodAlternates(alternateDeclaringTypes); + + for (List> parameterList : + JavaParserUtil.generateAllCombinationsForListOfMaps(parameters)) { + for (Map.Entry> returnType : + returnTypesToMustPreserveNodes.entrySet()) { + List params = parameterList.stream().map(Map.Entry::getKey).toList(); + Set toPreserve = new HashSet<>(); + for (Map.Entry entry : parameterList) { + Node node = entry.getValue(); + if (node != null) { + toPreserve.add(node); + } + } + + Node returnTypePreserve = returnType.getValue(); + + if (returnTypePreserve != null) { + toPreserve.add(returnTypePreserve); + } + + UnsolvedMethod method = + new UnsolvedMethod(name, returnType.getKey(), params, exceptions, toPreserve); + result.addAlternate(method); + } + } + + return result; + } + + /** + * Updates return types and must preserve nodes. Saves the intersection of the previous and the + * input, since we know more information to narrow potential return types down. + * + * @param returnsToPreserveNodes A map of return types to nodes that must be preserved + */ + public void updateReturnTypesAndMustPreserveNodes( + Map> returnsToPreserveNodes) { + // Update in-place; intersection = removing all elements in the original set + // that isn't found in the updated set + UnsolvedMethod old = getAlternates().get(0); + Set oldReturnTypes = getReturnTypes(); + getAlternates() + .removeIf(alternate -> !returnsToPreserveNodes.containsKey(alternate.getReturnType())); + + if (getAlternates().isEmpty() && oldReturnTypes.size() == 1) { + // If it's now empty and old return types was of size 1, it was probably a synthetic return + // type + for (Map.Entry> entry : + returnsToPreserveNodes.entrySet()) { + UnsolvedMethod method = + new UnsolvedMethod( + old.getName(), + entry.getKey(), + old.getParameterList(), + old.getThrownExceptions(), + Set.of(entry.getValue())); + addAlternate(method); + } + } + } + + @Override + public Set getFullyQualifiedNames() { + Set fqns = new LinkedHashSet<>(); + + for (UnsolvedMethod methodAlternate : getAlternates()) { + StringBuilder methodSignature = new StringBuilder(); + + methodSignature.append(methodAlternate.getName()).append('('); + + List parameterList = methodAlternate.getParameterList(); + for (int i = 0; i < parameterList.size(); i++) { + MemberType param = parameterList.get(i); + + // This is safe because all simple names are the same for unsolved types + // and there is only one FQN for solved types + methodSignature.append( + JavaParserUtil.getSimpleNameFromQualifiedName(JavaParserUtil.erase(param.toString()))); + + if (i + 1 < parameterList.size()) { + methodSignature.append(", "); + } + } + + methodSignature.append(')'); + + for (UnsolvedClassOrInterfaceAlternates alternate : getAlternateDeclaringTypes()) { + for (String fqn : alternate.getFullyQualifiedNames()) { + fqns.add(fqn + "#" + methodSignature.toString()); + } + } + } + + return fqns; + } + + /** Makes this method static. */ + @Override + public void setStatic() { + applyToAllAlternates(UnsolvedMethod::setStatic); + } + + /** + * Gets the number of type variables. + * + * @return The number of type variables + */ + @Override + public int getNumberOfTypeVariables() { + return getAlternates().get(0).getNumberOfTypeVariables(); + } + + /** + * Sets the number of type variables. + * + * @param number The number of type variables + */ + @Override + public void setNumberOfTypeVariables(int number) { + applyToAllAlternates(UnsolvedMethod::setNumberOfTypeVariables, number); + } + + /** + * Gets the return types + * + * @return The return types + */ + public Set getReturnTypes() { + return getAlternates().stream() + .map(alternate -> alternate.getReturnType()) + .collect(Collectors.toCollection(LinkedHashSet::new)); + } + + @Override + public String getName() { + return getAlternates().get(0).getName(); + } + + @Override + public List getThrownExceptions() { + return getAlternates().get(0).getThrownExceptions(); + } + + /** + * Use with caution: this method sets all alternates' return types to the same type. + * + *

{@inheritDoc} + */ + @Override + public void setReturnType(MemberType memberType) { + applyToAllAlternates(UnsolvedMethod::setReturnType, memberType); + } + + /** + * For all the alternates that have oldType as return type, replace it with newType. + * + * @param oldType The old return type + * @param newType The new return type + */ + public void replaceReturnType(MemberType oldType, MemberType newType) { + for (UnsolvedMethod alternate : getAlternates()) { + if (alternate.getReturnType().equals(oldType)) { + alternate.setReturnType(newType); + } + } + } + + @Override + public String getAccessModifier() { + return getAlternates().get(0).getAccessModifier(); + } + + @Override + public void setAccessModifier(String accessModifier) { + applyToAllAlternates(UnsolvedMethod::setAccessModifier, accessModifier); + } + + @Override + public boolean isStatic() { + return getAlternates().get(0).isStatic(); + } +} diff --git a/src/main/java/org/checkerframework/specimin/unsolved/UnsolvedMethodCommon.java b/src/main/java/org/checkerframework/specimin/unsolved/UnsolvedMethodCommon.java new file mode 100644 index 000000000..88a197787 --- /dev/null +++ b/src/main/java/org/checkerframework/specimin/unsolved/UnsolvedMethodCommon.java @@ -0,0 +1,69 @@ +package org.checkerframework.specimin.unsolved; + +import java.util.List; + +/** + * Common interface for {@link UnsolvedMethod} and {@link UnsolvedMethodAlternates}. Each getter + * should return the same value for each alternate; each setter should do the same operation to each + * alternate. If these requirements are not met, do not include the method in this interface. + */ +public interface UnsolvedMethodCommon { + /** + * Get the name of this method. + * + * @return the name of this method + */ + public String getName(); + + /** + * Getter for the throws list. + * + * @return the throws list + */ + public List getThrownExceptions(); + + /** + * Gets the access modifier (i.e., public, private) + * + * @return the access modifier + */ + public String getAccessModifier(); + + /** Makes this method static. */ + public void setStatic(); + + /** + * Returns true if this method is static + * + * @return True if the method is static + */ + public boolean isStatic(); + + /** + * Gets the number of type variables. + * + * @return The number of type variables + */ + public int getNumberOfTypeVariables(); + + /** + * Sets the number of type variables. + * + * @param number The number of type variables + */ + public void setNumberOfTypeVariables(int number); + + /** + * Sets the return type. + * + * @param memberType The return type + */ + public void setReturnType(MemberType memberType); + + /** + * Sets the access modifier (i.e., public, private) + * + * @param accessModifier The access modifier + */ + public void setAccessModifier(String accessModifier); +} diff --git a/src/main/java/org/checkerframework/specimin/unsolved/UnsolvedSymbolAlternate.java b/src/main/java/org/checkerframework/specimin/unsolved/UnsolvedSymbolAlternate.java new file mode 100644 index 000000000..6c5e62adb --- /dev/null +++ b/src/main/java/org/checkerframework/specimin/unsolved/UnsolvedSymbolAlternate.java @@ -0,0 +1,62 @@ +package org.checkerframework.specimin.unsolved; + +import com.github.javaparser.ast.Node; +import java.util.Collections; +import java.util.IdentityHashMap; +import java.util.Set; + +/** + * Base class for any SINGLE unsolved symbol alternate, such as UnsolvedClassOrInterface, + * UnsolvedMethod, or UnsolvedField. Contains a getter for setting nodes that must be preserved, + * given an alternate's definition. + * + *

{@link #getMustPreserveNodes()} is useful for certain ambiguities. For example: + * + *


+ * import org.example.Foo;
+ * class Simple {
+ *    void bar() {
+ *        Simple simple = new Simple(Foo.unsolvedMethod());
+ *    }
+ *
+ *    private Simple(String string) { }
+ *    private Simple(int x) { }
+ * }
+ * 
+ * + * {@code Foo.unsolvedMethod()} could return either String or int, so two alternates will be + * generated. Each alternate, based on its return type, will also preserve a different constructor, + * which is why this base class is necessary. + */ +public abstract class UnsolvedSymbolAlternate { + /** + * Super constructor for all inheriting classes to set mustPreserve nodes. + * + * @param mustPreserve The set of nodes that must be preserved for this alternate. + */ + public UnsolvedSymbolAlternate(Set mustPreserve) { + this.mustPreserve = Collections.newSetFromMap(new IdentityHashMap<>()); + this.mustPreserve.addAll(mustPreserve); + } + + /** A set of nodes that must be preserved for this alternate. */ + private final Set mustPreserve; + + /** + * Gets the nodes that must be preserved for this alternate. + * + * @return The nodes that must be preserved for this alternate + */ + public Set getMustPreserveNodes() { + return mustPreserve; + } + + /** + * Adds a node that must be preserved with this alternate. + * + * @param node The node to be preserved + */ + public void addMustPreserveNode(Node node) { + mustPreserve.add(node); + } +} diff --git a/src/main/java/org/checkerframework/specimin/unsolved/UnsolvedSymbolAlternates.java b/src/main/java/org/checkerframework/specimin/unsolved/UnsolvedSymbolAlternates.java new file mode 100644 index 000000000..ae68e63a9 --- /dev/null +++ b/src/main/java/org/checkerframework/specimin/unsolved/UnsolvedSymbolAlternates.java @@ -0,0 +1,151 @@ +package org.checkerframework.specimin.unsolved; + +import com.github.javaparser.ast.Node; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import java.util.function.BiConsumer; +import java.util.function.BiPredicate; +import java.util.function.Consumer; +import java.util.function.Predicate; + +/** Base type for all synthetic definitions containing alternates. */ +public abstract class UnsolvedSymbolAlternates { + /** A list of potential declaring types for this symbol. */ + private final List alternateDeclaringTypes; + + /** A list of alternate definitions for this symbol. */ + private List alternates = new ArrayList<>(); + + /** + * Base constructor for setting alternate declaring types. + * + * @param alternateDeclaringTypes The set of potential declaring types + */ + protected UnsolvedSymbolAlternates( + List alternateDeclaringTypes) { + this.alternateDeclaringTypes = new ArrayList<>(alternateDeclaringTypes); + } + + /** + * Gets all possible fully qualified names of this unsolved symbol definition. + * + * @return All possible FQNS + */ + public abstract Set getFullyQualifiedNames(); + + /** + * Gets all possible declaring types for this symbol. + * + * @return All possible declaring types + */ + public List getAlternateDeclaringTypes() { + return alternateDeclaringTypes; + } + + /** + * Gets alternate definitions for this symbol. + * + * @return All alternates + */ + public List getAlternates() { + return alternates; + } + + /** Removes duplicate alternates. */ + public void removeDuplicateAlternates() { + Set uniqueAlternates = new LinkedHashSet<>(alternates); + alternates.clear(); + alternates.addAll(uniqueAlternates); + } + + /** + * Utility method to apply a transformation to all alternates. For example, if you want to set all + * alternates to an interface, pass in {@code UnsolvedClassOrInterface::setIsAnInterfaceToTrue}. + * + * @param apply A Consumer that modifies each alternate. Pass in an instance method from {@link T} + * with no parameters. + */ + protected void applyToAllAlternates(Consumer apply) { + for (T alternate : alternates) { + apply.accept(alternate); + } + } + + /** + * Utility method to apply a transformation to all alternates. For example, if you want all + * alternates to extend type "Foo", pass in {@code UnsolvedClassOrInterface::extend} and "Foo". + * + * @param The type of the input parameter to the BiConsumer + * @param apply A BiConsumer that modifies each alternate. Pass in an instance method from {@link + * T} with one parameter. + * @param input The input to use to set all alternates. + */ + protected void applyToAllAlternates(BiConsumer apply, U input) { + for (T alternate : alternates) { + apply.accept(alternate, input); + } + } + + /** + * Returns true if all alternates return true for a predicate. You can pass in a method reference + * (like UnsolvedClassOrInterface::isAnInterface) to check if all alternates are an interface, for + * example. + * + * @param predicate A predicate; pass in an instance method from {@link T} with no parameters. + * @return True if all alternates return true for the predicate + */ + protected boolean doAllAlternatesReturnTrueFor(Predicate predicate) { + for (T alternate : alternates) { + if (!predicate.test(alternate)) { + return false; + } + } + return true; + } + + /** + * Returns true if all alternates return true for a predicate. You can pass in a method reference + * (like UnsolvedClassOrInterface::doesImplement) and an interface "MyInterface" to check if all + * alternates implement the "MyInterface" interface. + * + * @param The type of the input parameter to the BiPredicate + * @param predicate A BiPredicate; pass in an instance method from {@link T} with one parameter. + * @param input The input to use for the predicate + * @return True if all alternates return true for the predicate + */ + protected boolean doAllAlternatesReturnTrueFor(BiPredicate predicate, U input) { + for (T alternate : alternates) { + if (!predicate.test(alternate, input)) { + return false; + } + } + return true; + } + + /** + * Gets all the nodes that alternates could depend on. + * + * @return A set of all the nodes that alternates could depend on. + */ + public Set getDependentNodes() { + Set nodes = new HashSet<>(); + + for (T alternate : alternates) { + nodes.addAll(alternate.getMustPreserveNodes()); + } + + return nodes; + } + + /** + * Adds an alternate to this symbol's definition. + * + * @param alternate The alternate to add + */ + protected void addAlternate(T alternate) { + this.alternates.add(alternate); + } +} diff --git a/src/main/java/org/checkerframework/specimin/unsolved/UnsolvedSymbolEnumerator.java b/src/main/java/org/checkerframework/specimin/unsolved/UnsolvedSymbolEnumerator.java new file mode 100644 index 000000000..eea13f0ba --- /dev/null +++ b/src/main/java/org/checkerframework/specimin/unsolved/UnsolvedSymbolEnumerator.java @@ -0,0 +1,250 @@ +package org.checkerframework.specimin.unsolved; + +import com.github.javaparser.ast.Node; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; +import org.checkerframework.specimin.AmbiguityResolutionPolicy; +import org.checkerframework.specimin.JavaParserUtil; +import org.checkerframework.specimin.Slicer; + +/** + * Enumerates possible combinations of unsolved symbols, given a set of generated unsolved symbols + * from the {@link Slicer}. Depending on the {@link AmbiguityResolutionPolicy}, this class may + * enumerate one to all possibilities. + */ +public class UnsolvedSymbolEnumerator { + /** The unsolved types that must be included in the output. */ + private final Set unsolvedTypes = new LinkedHashSet<>(); + + /** The unsolved fields that must be included in the output. */ + private final Set unsolvedFields = new LinkedHashSet<>(); + + /** The unsolved methods that must be included in the output. */ + private final Set unsolvedMethods = new LinkedHashSet<>(); + + /** + * Creates a new instance of UnsolvedSymbolEnumerator. + * + * @param unsolvedSlice The slice of generated unsolved symbols, from the {@link Slicer}. + */ + public UnsolvedSymbolEnumerator(Set> unsolvedSlice) { + for (UnsolvedSymbolAlternates unsolvedSymbol : unsolvedSlice) { + if (unsolvedSymbol instanceof UnsolvedClassOrInterfaceAlternates type) { + unsolvedTypes.add(type); + } else if (unsolvedSymbol instanceof UnsolvedFieldAlternates field) { + unsolvedFields.add(field); + } else if (unsolvedSymbol instanceof UnsolvedMethodAlternates method) { + unsolvedMethods.add(method); + } + } + } + + /** + * Gets the best effort unsolved symbol generation. + * + * @param allDependentNodes The set of all nodes that are dependent on some alternate + * @return A map of class names to file content + */ + public UnsolvedSymbolEnumeratorResult getBestEffort(Set allDependentNodes) { + // Best effort is the first alternate in every alternate set + // This set should not contain any inner classes. + Set outerTypes = new LinkedHashSet<>(); + + // Note that the keyset is not equal to outerTypes. For Foo.Bar.Baz, Bar will be a key here, Baz + // will be an inner type; Foo will also be a key, Bar will be an inner type. However, outerTypes + // will only contain Foo. + Map> outerTypesToInnerTypes = + new LinkedHashMap<>(); + + for (UnsolvedClassOrInterfaceAlternates unsolved : unsolvedTypes) { + addTypeToCorrectDataStructure(unsolved, outerTypes, outerTypesToInnerTypes); + for (MemberType implemented : unsolved.getAlternates().get(0).getImplementedTypes()) { + addAllUsedTypesToSet(implemented, outerTypes, outerTypesToInnerTypes); + } + + MemberType extended = unsolved.getAlternates().get(0).getExtendedType(); + if (extended != null) { + addAllUsedTypesToSet(extended, outerTypes, outerTypesToInnerTypes); + } + } + + Map> typesToFields = new LinkedHashMap<>(); + + for (UnsolvedFieldAlternates unsolved : unsolvedFields) { + UnsolvedField field = unsolved.getAlternates().get(0); + UnsolvedClassOrInterfaceAlternates typeAlternates = + unsolved.getAlternateDeclaringTypes().get(0); + UnsolvedClassOrInterface type = typeAlternates.getAlternates().get(0); + if (!typesToFields.containsKey(type)) { + typesToFields.put(type, new LinkedHashSet<>()); + + addTypeToCorrectDataStructure(typeAlternates, outerTypes, outerTypesToInnerTypes); + } + + typesToFields.get(type).add(field); + + addAllUsedTypesToSet(field.getType(), outerTypes, outerTypesToInnerTypes); + } + + Map> typesToMethods = new LinkedHashMap<>(); + + for (UnsolvedMethodAlternates unsolved : unsolvedMethods) { + UnsolvedMethod method = unsolved.getAlternates().get(0); + UnsolvedClassOrInterfaceAlternates typeAlternates = + unsolved.getAlternateDeclaringTypes().get(0); + UnsolvedClassOrInterface type = typeAlternates.getAlternates().get(0); + if (!typesToMethods.containsKey(type)) { + typesToMethods.put(type, new LinkedHashSet<>()); + + addTypeToCorrectDataStructure(typeAlternates, outerTypes, outerTypesToInnerTypes); + } + + typesToMethods.get(type).add(method); + + addAllUsedTypesToSet(method.getReturnType(), outerTypes, outerTypesToInnerTypes); + + for (MemberType parameterType : method.getParameterList()) { + addAllUsedTypesToSet(parameterType, outerTypes, outerTypesToInnerTypes); + } + } + + Map result = new LinkedHashMap<>(); + + Set ableToRemove = new HashSet<>(allDependentNodes); + + for (UnsolvedClassOrInterface type : outerTypes) { + result.put( + type.getFullyQualifiedName(), + getTypeDeclarationAsString( + type, typesToFields, typesToMethods, outerTypesToInnerTypes, ableToRemove, false)); + } + + return new UnsolvedSymbolEnumeratorResult(result, ableToRemove); + } + + /** + * Gets the type declaration as a string, including all fields and methods. Also modifies the + * ableToRemove set by side effect. + * + * @param type The type to get the declaration for + * @param typesToFields A map of types to their fields + * @param typesToMethods A map of types to their methods + * @param outerTypesToInnerTypes A map of outer types to their inner types + * @param ableToRemove The set of nodes that can be removed in this iteration + * @param isInnerClass Whether the type is an inner class + * @return The type declaration as a string + */ + private String getTypeDeclarationAsString( + UnsolvedClassOrInterface type, + Map> typesToFields, + Map> typesToMethods, + Map> outerTypesToInnerTypes, + Set ableToRemove, + boolean isInnerClass) { + Set fields = typesToFields.get(type); + + if (fields == null) { + fields = Set.of(); + } + + Set methods = typesToMethods.get(type); + + if (methods == null) { + methods = Set.of(); + } + + Set innerTypes = outerTypesToInnerTypes.get(type); + + if (innerTypes == null) { + innerTypes = Set.of(); + } + + ableToRemove.removeAll(type.getMustPreserveNodes()); + + for (UnsolvedField field : fields) { + ableToRemove.removeAll(field.getMustPreserveNodes()); + } + + for (UnsolvedMethod method : methods) { + ableToRemove.removeAll(method.getMustPreserveNodes()); + } + + return type.toString( + methods, + fields, + innerTypes.stream() + .map( + inner -> + getTypeDeclarationAsString( + inner, + typesToFields, + typesToMethods, + outerTypesToInnerTypes, + ableToRemove, + true)) + .toList(), + isInnerClass); + } + + /** + * Given a MemberType, recursively adds all used UnsolvedClassOrInterface types to the correct + * data structure by calling {@link #addTypeToCorrectDataStructure}. + * + * @param memberType The member type + * @param types The set to add to + */ + private void addAllUsedTypesToSet( + MemberType memberType, + Set types, + Map> outerTypesToInnerTypes) { + for (MemberType typeArg : memberType.getTypeArguments()) { + addAllUsedTypesToSet(typeArg, types, outerTypesToInnerTypes); + } + + if (memberType instanceof UnsolvedMemberType unsolvedType) { + addTypeToCorrectDataStructure(unsolvedType.getUnsolvedType(), types, outerTypesToInnerTypes); + } else if (memberType instanceof WildcardMemberType wildcardType) { + MemberType bound = wildcardType.getBound(); + + if (bound != null) { + addAllUsedTypesToSet(bound, types, outerTypesToInnerTypes); + } + } + } + + /** + * If an UnsolvedClassOrInterfaceAlternates type is an inner class, it is added to the + * outerTypesToInnerTypes map. If it is an outer class, it is added to the outerTypes set. + * + * @param type The type + * @param outerTypes The set of outer types + * @param outerTypesToInnerTypes The map of outer types to their inner types + */ + private void addTypeToCorrectDataStructure( + UnsolvedClassOrInterfaceAlternates type, + Set outerTypes, + Map> outerTypesToInnerTypes) { + UnsolvedClassOrInterface alternate = type.getAlternates().get(0); + + // Alternate declaring types may not be empty but the first alternate could still be an outer + // type. This could happen when Foo is not imported, so Foo could either be located in the + // unsolved parent class or in the same package. + if (type.getAlternateDeclaringTypes().isEmpty() + || (JavaParserUtil.isAClassPath(alternate.getFullyQualifiedName()) + && JavaParserUtil.isProbablyAPackage( + alternate + .getFullyQualifiedName() + .substring(0, alternate.getFullyQualifiedName().lastIndexOf('.'))))) { + outerTypes.add(alternate); + } else { + for (UnsolvedClassOrInterfaceAlternates declaringType : type.getAlternateDeclaringTypes()) { + outerTypesToInnerTypes + .computeIfAbsent(declaringType.getAlternates().get(0), k -> new LinkedHashSet<>()) + .add(alternate); + } + } + } +} diff --git a/src/main/java/org/checkerframework/specimin/unsolved/UnsolvedSymbolEnumeratorResult.java b/src/main/java/org/checkerframework/specimin/unsolved/UnsolvedSymbolEnumeratorResult.java new file mode 100644 index 000000000..321a42c92 --- /dev/null +++ b/src/main/java/org/checkerframework/specimin/unsolved/UnsolvedSymbolEnumeratorResult.java @@ -0,0 +1,14 @@ +package org.checkerframework.specimin.unsolved; + +import com.github.javaparser.ast.Node; +import java.util.Map; +import java.util.Set; + +/** + * Represents a result from the UnsolvedSymbolEnumerator. {@link #classNamesToFileContent()} + * represents a map of generated symbols' fully qualified names to their file content, and {@link + * #unusedDependentNodes()} represents Nodes not used in this iteration of UnsolvedSymbolEnumerator, + * which are safe to remove. + */ +public record UnsolvedSymbolEnumeratorResult( + Map classNamesToFileContent, Set unusedDependentNodes) {} diff --git a/src/main/java/org/checkerframework/specimin/unsolved/UnsolvedSymbolGenerator.java b/src/main/java/org/checkerframework/specimin/unsolved/UnsolvedSymbolGenerator.java new file mode 100644 index 000000000..b9a376d33 --- /dev/null +++ b/src/main/java/org/checkerframework/specimin/unsolved/UnsolvedSymbolGenerator.java @@ -0,0 +1,3397 @@ +package org.checkerframework.specimin.unsolved; + +import com.github.javaparser.ast.AccessSpecifier; +import com.github.javaparser.ast.CompilationUnit; +import com.github.javaparser.ast.Node; +import com.github.javaparser.ast.NodeList; +import com.github.javaparser.ast.body.CallableDeclaration; +import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration; +import com.github.javaparser.ast.body.EnumConstantDeclaration; +import com.github.javaparser.ast.body.EnumDeclaration; +import com.github.javaparser.ast.body.FieldDeclaration; +import com.github.javaparser.ast.body.MethodDeclaration; +import com.github.javaparser.ast.body.Parameter; +import com.github.javaparser.ast.body.TypeDeclaration; +import com.github.javaparser.ast.body.VariableDeclarator; +import com.github.javaparser.ast.expr.AnnotationExpr; +import com.github.javaparser.ast.expr.ArrayInitializerExpr; +import com.github.javaparser.ast.expr.AssignExpr; +import com.github.javaparser.ast.expr.ClassExpr; +import com.github.javaparser.ast.expr.Expression; +import com.github.javaparser.ast.expr.FieldAccessExpr; +import com.github.javaparser.ast.expr.InstanceOfExpr; +import com.github.javaparser.ast.expr.LambdaExpr; +import com.github.javaparser.ast.expr.MemberValuePair; +import com.github.javaparser.ast.expr.MethodCallExpr; +import com.github.javaparser.ast.expr.MethodReferenceExpr; +import com.github.javaparser.ast.expr.NameExpr; +import com.github.javaparser.ast.expr.NormalAnnotationExpr; +import com.github.javaparser.ast.expr.ObjectCreationExpr; +import com.github.javaparser.ast.expr.PatternExpr; +import com.github.javaparser.ast.expr.SingleMemberAnnotationExpr; +import com.github.javaparser.ast.expr.TypeExpr; +import com.github.javaparser.ast.expr.VariableDeclarationExpr; +import com.github.javaparser.ast.nodeTypes.NodeWithArguments; +import com.github.javaparser.ast.stmt.CatchClause; +import com.github.javaparser.ast.stmt.ExplicitConstructorInvocationStmt; +import com.github.javaparser.ast.stmt.ReturnStmt; +import com.github.javaparser.ast.stmt.ThrowStmt; +import com.github.javaparser.ast.stmt.TryStmt; +import com.github.javaparser.ast.type.ClassOrInterfaceType; +import com.github.javaparser.ast.type.IntersectionType; +import com.github.javaparser.ast.type.ReferenceType; +import com.github.javaparser.ast.type.Type; +import com.github.javaparser.ast.type.TypeParameter; +import com.github.javaparser.resolution.Resolvable; +import com.github.javaparser.resolution.UnsolvedSymbolException; +import com.github.javaparser.resolution.declarations.ResolvedMethodDeclaration; +import com.github.javaparser.resolution.declarations.ResolvedMethodLikeDeclaration; +import com.github.javaparser.resolution.declarations.ResolvedParameterDeclaration; +import com.github.javaparser.resolution.declarations.ResolvedReferenceTypeDeclaration; +import com.github.javaparser.resolution.declarations.ResolvedTypeParameterDeclaration; +import com.github.javaparser.resolution.declarations.ResolvedValueDeclaration; +import com.github.javaparser.resolution.types.ResolvedReferenceType; +import com.github.javaparser.resolution.types.ResolvedType; +import com.github.javaparser.utils.Pair; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.specimin.JavaLangUtils; +import org.checkerframework.specimin.JavaParserUtil; + +/** + * Generates unsolved symbols. This class ensures that only one of each type is created; i.e., the + * same FQNs will point to the same instance. More symbols are tracked here than returned into the + * final slice; this is to ensure classes used by some alternates are only outputted when those + * alternates are selected. + */ +public class UnsolvedSymbolGenerator { + /** A map of fully qualified names to their corresponding compilation units. */ + private final Map fqnsToCompilationUnits; + + /** Generates fully qualified names for symbols. */ + private final FullyQualifiedNameGenerator fullyQualifiedNameGenerator; + + /** + * Creates a new UnsolvedSymbolGenerator. Pass in a set of fqns to compilation units for + * resolution purposes. + * + * @param fqnsToCompilationUnits A set of fully-qualified names to compilation units + */ + public UnsolvedSymbolGenerator(Map fqnsToCompilationUnits) { + this.fqnsToCompilationUnits = fqnsToCompilationUnits; + fullyQualifiedNameGenerator = + new FullyQualifiedNameGenerator(fqnsToCompilationUnits, generatedSymbols); + } + + /** + * The cache of unsolved symbol definitions. These values need not be unique; the map is provided + * for simple lookups when adding new symbols. Keys: fully qualified names --> values: unsolved + * symbol alternates + */ + private final Map> generatedSymbols = new HashMap<>(); + + /** + * Gets all generated symbols. + * + * @return The map of fqns to generated symbols. + */ + public Map> getGeneratedSymbols() { + return generatedSymbols; + } + + /** + * Contains all methods that still have null as a parameter type. When encountering a new method + * signature that replaces each null with a type, remove it from this list and also from + * generatedSymbols. If one is never found, then replace all instances of null with + * java.lang.Object. + */ + private final Set methodsWithNullInSignature = new HashSet<>(); + + /** + * Given an unresolvable Node, generate a corresponding synthetic definition. In cases where + * multiple nodes are not known (for example, the node is a field A.b and both type A and field b + * are not resolvable), this method will recursively call itself and return both generated + * symbols. + * + * @param node The unresolvable node + * @return A list of UnsolvedSymbolAlternates generated/found from the input + */ + public List> inferContext(Node node) { + List> generated = new ArrayList<>(); + inferContextImpl(node, generated); + + return generated; + } + + /** + * Unsolved symbols are added to result. The member generated/found based on {@code node} is added + * in addition to any types in its scope. Only items that must be included in the final output + * should be added to result. + * + * @param node The node + * @param result The list of generated/found symbols, according to the rules above + */ + private void inferContextImpl(Node node, List> result) { + // https://www.javadoc.io/doc/com.github.javaparser/javaparser-core/latest/com/github/javaparser/resolution/Resolvable.html + + // Ignore declarations in this method. If a declaration is not resolvable, it's probably because + // a member is not resolvable. But, the type dependency map will eventually reach it, so the + // symbol will eventually be generated anyway. + + // Also ignore nodes like ArrayType or IntersectionType because the type rule dependency map + // will also break down its types. + + // Types + if (node instanceof ClassOrInterfaceType asType) { + handleClassOrInterfaceType(asType, result); + } else if (node instanceof AnnotationExpr asAnno) { + handleAnnotationExpr(asAnno, result); + } else if (node instanceof IntersectionType intersection) { + for (ReferenceType type : intersection.getElements()) { + inferContextImpl(type, result); + } + } else if (node instanceof TypeExpr typeExpr) { + inferContextImpl(typeExpr.getType(), result); + } + // Fields (although some types are handled as FieldAccessExpr or NameExpr too) + else if (node instanceof FieldAccessExpr asField) { + handleFieldAccessExpr(asField, result); + } else if (node instanceof NameExpr nameExpr) { + handleNameExpr(nameExpr, result); + } + // Methods + else if (node instanceof MethodCallExpr methodCall) { + handleMethodCallExpr(methodCall, result); + } else if (node instanceof ObjectCreationExpr + || node instanceof ExplicitConstructorInvocationStmt) { + UnsolvedClassOrInterfaceAlternates scope; + String constructorName; + List arguments; + int numberOfTypeParams = 0; + + if (node instanceof ObjectCreationExpr constructor) { + try { + constructor.calculateResolvedType(); + // If the type is resolvable, the constructor is too; a type in the constructor is not + // solvable. Return because we don't need to generate a new constructor. + return; + } catch (UnsolvedSymbolException ex) { + // continue + } + + inferContextImpl(constructor.getType(), result); + // Do not generate here; that should be taken care of in the inferContextImpl call above. + scope = + (UnsolvedClassOrInterfaceAlternates) + findExistingAndUpdateFQNs( + fullyQualifiedNameGenerator.getFQNsFromType(constructor.getType())); + + constructorName = constructor.getTypeAsString(); + arguments = constructor.getArguments(); + + // While rare, constructors can have type parameters, just like how a method can define + // its own. + if (constructor.getTypeArguments().isPresent()) { + numberOfTypeParams = constructor.getTypeArguments().get().size(); + } + } else { + ExplicitConstructorInvocationStmt constructor = (ExplicitConstructorInvocationStmt) node; + + // If it's unresolvable, it's a constructor in the unsolved parent class + if (!constructor.isThis()) { + // There can only be one extends in a class + ClassOrInterfaceType superClass = JavaParserUtil.getSuperClass(node); + + try { + superClass.resolve(); + // If the type is resolvable, the constructor is too; a type in the constructor is not + // solvable. Return because we don't need to generate a new constructor. + return; + } catch (UnsolvedSymbolException ex) { + // continue + } + + inferContextImpl(superClass, result); + // Do not generate here; that should be taken care of in the inferContextImpl call above. + scope = + (UnsolvedClassOrInterfaceAlternates) + findExistingAndUpdateFQNs( + fullyQualifiedNameGenerator.getFQNsFromType(superClass)); + + constructorName = superClass.getNameAsString(); + arguments = constructor.getArguments(); + } else { + // We should never reach this case unless the user inputted a bad program (i.e. + // this(...) constructor call when a definition is not there, or super() without a parent + // class) + throw new RuntimeException("Unexpected explicit constructor invocation statement call."); + } + } + + if (scope == null) { + throw new RuntimeException( + "Scope was not generated in constructor call when it should have been."); + } + + // A constructor call indicates a class + scope.setType(UnsolvedClassOrInterfaceType.CLASS); + + handleConstructorCall( + scope, JavaParserUtil.erase(constructorName), arguments, numberOfTypeParams, result); + } else if (node instanceof MethodDeclaration methodDecl) { + handleMethodDeclarationWithOverride(methodDecl, result); + } + // Method references + else if (node instanceof MethodReferenceExpr methodRef) { + handleMethodReferenceExpr(methodRef, result); + } + // A lambda expr is not of type Resolvable, but it could be passed into this method + // when an argument is a lambda. + else if (node instanceof LambdaExpr lambda) { + handleLambdaExpr(lambda, result); + } + // May be passed into the method if in an annotation. + else if (node instanceof ClassExpr classExpr) { + inferContextImpl(classExpr.getType(), result); + } else if (node instanceof ArrayInitializerExpr arrayInitializerExpr) { + for (Expression value : arrayInitializerExpr.getValues()) { + inferContextImpl(value, result); + } + } + } + + /** + * Helper method for {@link #inferContextImpl(Node, List)}. Handles ClassOrInterfaceType: adds the + * existing definition to the result if found, or a new definition if one does not already exist. + * + * @param type The type to handle + * @param result The result of inferContext + */ + private void handleClassOrInterfaceType( + ClassOrInterfaceType type, List> result) { + try { + ResolvedType resolved = type.resolve(); + + if (resolved.isTypeVariable()) { + TypeParameter typeParam = + (TypeParameter) + JavaParserUtil.tryFindAttachedNode( + resolved.asTypeParameter(), fqnsToCompilationUnits); + + if (typeParam != null) { + for (ClassOrInterfaceType bound : typeParam.getTypeBound()) { + inferContextImpl(bound, result); + } + } + } + + return; + } catch (UnsolvedSymbolException ex) { + // Ok to continue + } + FullyQualifiedNameSet potentialFQNs = fullyQualifiedNameGenerator.getFQNsFromType(type); + + // ClassOrInterfaceType may be Set, which would be unresolvable because of + // UnknownType, but we should not create Set in this case. + if (doesOverlapWithKnownType(potentialFQNs.erasedFqns())) { + return; + } + + UnsolvedClassOrInterfaceAlternates generated = + findExistingAndUpdateFQNsOrCreateNewType(potentialFQNs.erasedFqns()); + + if (generated.getTypeVariables().isEmpty() && type.getTypeArguments().isPresent()) { + generated.setTypeVariables(type.getTypeArguments().get().size()); + + NodeList typeArgs = type.getTypeArguments().get(); + List typeArgsPreferred = new ArrayList<>(generated.getTypeVariables()); + + boolean changed = false; + + for (int i = 0; i < typeArgs.size(); i++) { + Type typeArg = typeArgs.get(i); + + boolean isTypeParameter = false; + try { + isTypeParameter = typeArg.resolve().isTypeVariable(); + } catch (UnsolvedSymbolException ex) { + // Ok to continue + } + + if (isTypeParameter) { + typeArgsPreferred.set(i, typeArg.resolve().asTypeParameter().getName()); + changed = true; + } + } + + if (changed) { + generated.setTypeVariables(typeArgsPreferred); + } + } + + result.add(generated); + + // If this type is A, and A is in an extends clause of a non-abstract class, and that class + // also implements JDK interfaces, and the current declaration has no implementations of must + // implement methods, we need to generate these methods here. + if (type.getParentNode().isPresent() + && type.getParentNode().get() instanceof ClassOrInterfaceDeclaration parent + && !parent.isInterface() + && !parent.isAbstract() + && parent.getExtendedTypes().contains(type)) { + Set withNoDeclaration = + JavaParserUtil.getMustImplementMethodsWithNoExistingDeclaration( + parent, fqnsToCompilationUnits); + + for (ResolvedMethodDeclaration method : withNoDeclaration) { + Set methodFQNs = new LinkedHashSet<>(); + + String signature = method.getName() + "("; + List> paramTypes = new ArrayList<>(); + + for (int i = 0; i < method.getNumberOfParams(); i++) { + signature += + JavaParserUtil.getSimpleNameFromQualifiedName( + JavaParserUtil.erase(method.getParam(i).toString())); + if (i < method.getNumberOfParams() - 1) { + signature += ", "; + } + + paramTypes.add(Set.of(new SolvedMemberType(method.getParam(i).describeType()))); + } + + signature += ")"; + + for (String parentFQN : generated.getFullyQualifiedNames()) { + methodFQNs.add(parentFQN + "#" + signature); + } + + UnsolvedMethodAlternates gen = + (UnsolvedMethodAlternates) findExistingAndUpdateFQNs(methodFQNs); + + if (gen == null) { + gen = + UnsolvedMethodAlternates.create( + method.getName(), + Set.of(new SolvedMemberType(method.getReturnType().describe())), + List.of(generated), + paramTypes); + addNewSymbolToGeneratedSymbolsMap(gen); + result.add(gen); + } + } + } + } + + /** + * Helper method for {@link #inferContextImpl(Node, List)}. Handles annotations: adds the existing + * definition to the result if found, or a new definition if one does not already exist. + * + * @param anno The annotation to handle + * @param result The result of inferContext + */ + private void handleAnnotationExpr(AnnotationExpr anno, List> result) { + // TODO: handle default values when necessary + try { + anno.resolve(); + return; + } catch (UnsolvedSymbolException ex) { + // Ok to continue + } + FullyQualifiedNameSet potentialFQNs = fullyQualifiedNameGenerator.getFQNsFromAnnotation(anno); + + UnsolvedClassOrInterfaceAlternates generated = + findExistingAndUpdateFQNsOrCreateNewType(potentialFQNs.erasedFqns()); + generated.setType(UnsolvedClassOrInterfaceType.ANNOTATION); + + result.add(generated); + + // According to JLS 9.6.1 + // (https://docs.oracle.com/javase/specs/jls/se8/html/jls-9.html#jls-9.6.1): + // * A primitive type + // * String + // * Class or an invocation of Class (§4.5) + // * An enum type + // * An annotation type + // * An array type whose component type is one of the preceding types + // Nested arrays are not valid + if (anno instanceof SingleMemberAnnotationExpr singleMemberAnnotationExpr) { + result.add( + findOrGenerateAnnotationMemberValueMethod( + singleMemberAnnotationExpr.getMemberValue(), "value", generated, result)); + } else if (anno instanceof NormalAnnotationExpr normalAnnotationExpr) { + for (MemberValuePair memberValuePair : normalAnnotationExpr.getPairs()) { + result.add( + findOrGenerateAnnotationMemberValueMethod( + memberValuePair.getValue(), memberValuePair.getNameAsString(), generated, result)); + } + } + } + + /** + * Given a member value in an annotation, generate/update a method that represents it in an + * annotation declaration and return it. + * + * @param annotationMemberValue The annotation member value + * @param name The name of the annotation member value pair + * @param annotation The annotation to hold this definition + * @param result The result list + * @return The generated/found method that represents this member value + */ + private UnsolvedMethodAlternates findOrGenerateAnnotationMemberValueMethod( + Expression annotationMemberValue, + String name, + UnsolvedClassOrInterfaceAlternates annotation, + List> result) { + inferContextImpl(annotationMemberValue, result); + + Expression toLookUpTypeFor = annotationMemberValue; + boolean isArray = false; + boolean isEmpty = false; + if (toLookUpTypeFor.isArrayInitializerExpr()) { + isArray = true; + if (toLookUpTypeFor.asArrayInitializerExpr().getValues().isNonEmpty()) { + toLookUpTypeFor = toLookUpTypeFor.asArrayInitializerExpr().getValues().get(0); + } else { + isEmpty = true; + } + } + + FullyQualifiedNameSet fqns; + if (isEmpty) { + // Handle empty arrays (i.e. @Anno({})); we have no way of telling + // what it actually is + fqns = new FullyQualifiedNameSet(Set.of("java.lang.String[]")); + } else { + FullyQualifiedNameSet rawFqns; + + try { + if (toLookUpTypeFor.isAnnotationExpr()) { + rawFqns = + new FullyQualifiedNameSet( + Set.of(toLookUpTypeFor.asAnnotationExpr().resolve().getQualifiedName())); + } else if (toLookUpTypeFor instanceof FieldAccessExpr fieldAccess + && JavaParserUtil.looksLikeAConstant(fieldAccess.getNameAsString())) { + // If it looks like an enum, it probably is + rawFqns = + fullyQualifiedNameGenerator + .getFQNsForExpressionType(fieldAccess.getScope()) + .iterator() + .next(); + } else { + rawFqns = + fullyQualifiedNameGenerator + .getFQNsForExpressionType(toLookUpTypeFor) + .iterator() + .next(); + } + } catch (UnsolvedSymbolException ex) { + rawFqns = + fullyQualifiedNameGenerator.getFQNsForExpressionType(toLookUpTypeFor).iterator().next(); + } + + List typeArgs = List.of(); + Set fqnsAsString = new LinkedHashSet<>(); + for (String fqn : rawFqns.erasedFqns()) { + // java.lang.Class<...> --> java.lang.Class + if (fqn.equals("java.lang.Class")) { + typeArgs = List.of(FullyQualifiedNameSet.UNBOUNDED_WILDCARD); + } + + if (isArray) { + fqn += "[]"; + } + + fqnsAsString.add(fqn); + } + + fqns = new FullyQualifiedNameSet(fqnsAsString, typeArgs); + } + + MemberType type = getMemberTypeFromFQNs(fqns, false); + + if (type == null) { + throw new RuntimeException("Annotation member value type must have been generated: " + fqns); + } + + Set methodFQNs = new LinkedHashSet<>(); + + for (String parentFQN : annotation.getFullyQualifiedNames()) { + methodFQNs.add(parentFQN + "#" + name + "()"); + } + + UnsolvedMethodAlternates gen = (UnsolvedMethodAlternates) findExistingAndUpdateFQNs(methodFQNs); + + if (gen == null) { + gen = UnsolvedMethodAlternates.create(name, Set.of(type), List.of(annotation), List.of()); + } + // If it was created before, the last time could have been an empty array and defaulted to + // String[]. This will correct it + // if we discover a type. + else if (annotationMemberValue.isArrayInitializerExpr() + && annotationMemberValue.asArrayInitializerExpr().getValues().isNonEmpty()) { + gen.setReturnType(type); + } + + return gen; + } + + /** + * Helper method for {@link #inferContextImpl(Node, List)}. This method handles cases where + * FieldAccessExpr could be either a type or a field (when getting the scope of a FieldAccessExpr, + * it may return another FieldAccessExpr in the form of a class path). Adds the existing + * definition to the result if found, or a new definition if one does not already exist. + * + * @param field The field to handle + * @param result The result of inferContext + */ + private void handleFieldAccessExpr( + FieldAccessExpr field, List> result) { + // It may be solvable (when passed into this method via scope) + // In this case, while the declaration may be solvable, the type may not be + try { + ResolvedValueDeclaration resolved = field.resolve(); + + Type type = + JavaParserUtil.getTypeFromResolvedValueDeclaration(resolved, fqnsToCompilationUnits); + + if (type != null) { + inferContextImpl(type, result); + } + + return; + } catch (UnsolvedSymbolException ex) { + // If the declaration is not resolvable, then check to see if it is a + // known class that has been passed in + if (JavaParserUtil.isExprTypeResolvable(field)) { + // This is most likely a class; resolve() only works on field declarations. + // System.out, for example, would fail to resolve() but calculateResolvedType() would work. + return; + } + } + + // When we have a FieldAccessExpr like a.b.c, the scope a.b is also a FieldAccessExpr + // We need to handle the case where the scope could be a class, like org.example.MyClass, + // because resolving the scope of a static field like org.example.MyClass.a would return + // another FieldAccessExpr, not a ClassOrInterfaceType + if (JavaParserUtil.isAClassPath(field.toString())) { + for (FullyQualifiedNameSet potentialFQNs : + fullyQualifiedNameGenerator.getFQNsForExpressionType(field)) { + UnsolvedClassOrInterfaceAlternates generated = + findExistingAndUpdateFQNsOrCreateNewType(potentialFQNs.erasedFqns()); + + result.add(generated); + } + return; + } + + Collection> potentialScopeFQNs = + fullyQualifiedNameGenerator.getFQNsForExpressionLocation(field); + + Expression scope = field.getScope(); + + // Special case: handle this/super separately since potentialScopeFQNs + // provides more information than a this/super expression alone in + // inferContextImpl + if (scope.isThisExpr() || scope.isSuperExpr()) { + handleThisOrSuperExpr(potentialScopeFQNs); + } else { + // Generate everything in the scopes before + inferContextImpl(scope, result); + } + + // Could be empty if the field is called on a NameExpr with a union type, + // but the field is located in a known class. + if (potentialScopeFQNs.isEmpty()) { + return; + } + + Set potentialFQNs = new LinkedHashSet<>(); + + for (Set set : potentialScopeFQNs) { + for (String potentialScopeFQN : set) { + potentialFQNs.add(potentialScopeFQN + "#" + field.getNameAsString()); + } + } + + Map> typeToMustPreserveNode = + getTypeToCallableDeclarationFromArgument(field); + + UnsolvedSymbolAlternates alreadyGenerated = findExistingAndUpdateFQNs(potentialFQNs); + + if (!(alreadyGenerated instanceof UnsolvedFieldAlternates)) { + // Since we called inferContextImpl(scope), the field's parents are created + List potentialParents = new ArrayList<>(); + for (Set set : potentialScopeFQNs) { + UnsolvedSymbolAlternates generated = findExistingAndUpdateFQNs(set); + + if (generated == null) { + throw new RuntimeException("Field scope types are not yet created; FQNs: " + set); + } + potentialParents.add((UnsolvedClassOrInterfaceAlternates) generated); + } + + @SuppressWarnings("unchecked") + boolean isInAnnotation = field.findAncestor(AnnotationExpr.class).isPresent(); + + if (isInAnnotation) { + potentialParents.forEach(parent -> parent.setType(UnsolvedClassOrInterfaceType.ENUM)); + } + + boolean isStatic = JavaParserUtil.getFQNIfStaticMember(field) != null; + + UnsolvedFieldAlternates createdField; + if (typeToMustPreserveNode.isEmpty()) { + Set types = + isInAnnotation ? Set.of(new SolvedMemberType("")) : new LinkedHashSet<>(); + + if (!isInAnnotation) { + for (FullyQualifiedNameSet potentialTypeFQNs : + fullyQualifiedNameGenerator.getFQNsForExpressionType(field)) { + types.add(getOrCreateMemberTypeFromFQNs(potentialTypeFQNs)); + } + } + + createdField = + UnsolvedFieldAlternates.create( + field.getNameAsString(), types, potentialParents, isStatic, false); + } else { + createdField = + UnsolvedFieldAlternates.create( + field.getNameAsString(), typeToMustPreserveNode, potentialParents, isStatic, false); + } + + addNewSymbolToGeneratedSymbolsMap(createdField); + result.add(createdField); + } else if (!typeToMustPreserveNode.isEmpty()) { + ((UnsolvedFieldAlternates) alreadyGenerated) + .updateFieldTypesAndMustPreserveNodes(typeToMustPreserveNode); + } + } + + /** + * Helper method for {@link #inferContextImpl(Node, List)}. Adds the existing definition to the + * result if found, or a new definition if one does not already exist. This method handles cases + * where NameExpr could be either a type or a field (when getting the scope of a FieldAccessExpr, + * it may return a NameExpr in the form of a class name, indicated by a capital). Adds the + * existing definition to the result if found, or a new definition if one does not already exist. + * + * @param nameExpr The field/variable to handle + * @param result The result of inferContext + */ + private void handleNameExpr(NameExpr nameExpr, List> result) { + // resolvable (when passed into this method via scope) + // In this case, while the declaration may be solvable, the type may not be + try { + ResolvedValueDeclaration resolved = nameExpr.resolve(); + + Type type = + JavaParserUtil.getTypeFromResolvedValueDeclaration(resolved, fqnsToCompilationUnits); + + if (type != null) { + inferContextImpl(type, result); + + if (type.isUnknownType()) { + // If unknown type, generate synthetic types for it + for (FullyQualifiedNameSet fqns : + fullyQualifiedNameGenerator.getFQNsForExpressionType(nameExpr)) { + findExistingAndUpdateFQNsOrCreateNewType(fqns.erasedFqns()); + } + } + } + + return; + } catch (UnsolvedSymbolException ex) { + // If the declaration is not resolvable, then check to see if it is a + // known class that has been passed in + if (JavaParserUtil.isExprTypeResolvable(nameExpr)) { + // This is most likely a class; resolve() only works on field/variable declarations. + // System, for example, would fail to resolve() but calculateResolvedType() would work. + return; + } + + if (JavaParserUtil.tryResolveExpressionIfInAnonymousClass(nameExpr) != null) { + return; + } + + FieldDeclaration field = + (FieldDeclaration) + JavaParserUtil.tryFindCorrespondingDeclarationInAnonymousClass(nameExpr); + if (field != null) { + inferContextImpl(field.getElementType(), result); + return; + } + } + + // class name + if (JavaParserUtil.isAClassName(nameExpr.getNameAsString())) { + for (FullyQualifiedNameSet potentialFQNs : + fullyQualifiedNameGenerator.getFQNsForExpressionType(nameExpr)) { + UnsolvedClassOrInterfaceAlternates generated = + findExistingAndUpdateFQNsOrCreateNewType(potentialFQNs.erasedFqns()); + + result.add(generated); + } + return; + } + + Collection> parentClassFQNs = + fullyQualifiedNameGenerator.getFQNsForExpressionLocation(nameExpr); + Set fieldFQNs = new LinkedHashSet<>(); + + for (Set set : parentClassFQNs) { + for (String parentClassFQN : set) { + fieldFQNs.add(parentClassFQN + "#" + nameExpr.getNameAsString()); + } + } + + Map> typeToMustPreserveNode = + getTypeToCallableDeclarationFromArgument(nameExpr); + + UnsolvedSymbolAlternates generatedField = findExistingAndUpdateFQNs(fieldFQNs); + + if (!(generatedField instanceof UnsolvedFieldAlternates)) { + // Generate/find the class that will hold the field + List generatedClasses = new ArrayList<>(); + + for (Set fqns : parentClassFQNs) { + generatedClasses.add(findExistingAndUpdateFQNsOrCreateNewType(fqns)); + } + + // NameExpr and static import must be static and final + boolean isStaticImport = JavaParserUtil.getFQNIfStaticMember(nameExpr) != null; + + if (typeToMustPreserveNode.isEmpty()) { + Set memberTypes = new LinkedHashSet<>(); + + for (FullyQualifiedNameSet typeFQNs : + fullyQualifiedNameGenerator.getFQNsForExpressionType(nameExpr)) { + MemberType type = getOrCreateMemberTypeFromFQNs(typeFQNs); + + memberTypes.add(type); + } + + generatedField = + UnsolvedFieldAlternates.create( + nameExpr.getNameAsString(), + memberTypes, + generatedClasses, + isStaticImport, + isStaticImport); + } else { + generatedField = + UnsolvedFieldAlternates.create( + nameExpr.getNameAsString(), + typeToMustPreserveNode, + generatedClasses, + isStaticImport, + isStaticImport); + } + + addNewSymbolToGeneratedSymbolsMap(generatedField); + + result.add(generatedField); + } else if (!typeToMustPreserveNode.isEmpty()) { + ((UnsolvedFieldAlternates) generatedField) + .updateFieldTypesAndMustPreserveNodes(typeToMustPreserveNode); + } + } + + /** + * Helper method for {@link #inferContextImpl(Node, List)}. Adds the existing definition to the + * result if found, or a new definition if one does not already exist. + * + * @param methodCall The method call to handle + * @param result The result of inferContext + */ + private void handleMethodCallExpr( + MethodCallExpr methodCall, List> result) { + try { + ResolvedMethodDeclaration resolvedMethodDeclaration = methodCall.resolve(); + + // If we're here, this was probably passed in as scope/argument + Node node = + JavaParserUtil.tryFindAttachedNode(resolvedMethodDeclaration, fqnsToCompilationUnits); + + if (node != null) { + MethodDeclaration toAst = (MethodDeclaration) node; + + inferContextImpl(toAst.getType(), result); + } + + return; + } catch (UnsolvedSymbolException ex) { + if (JavaParserUtil.tryResolveExpressionIfInAnonymousClass(methodCall) != null) { + return; + } + } catch (UnsupportedOperationException ex) { + // continue + } + + List> definitions = + JavaParserUtil.tryResolveNodeWithUnresolvableArguments(methodCall, fqnsToCompilationUnits); + + if (!definitions.isEmpty() + && methodCall.getArguments().stream() + .allMatch( + arg -> + JavaParserUtil.isExprTypeResolvable(arg) + || JavaParserUtil.isExprDefinitionResolvable(arg))) { + // Special case: method declaration is findable, arguments are all solvable, but a parameter + // type is not. In this case, the type of the parameters are unsolved, and should be preserved + // if the parameter type ever ends up becoming used (which it will, after addInformation is + // done). + for (CallableDeclaration callable : definitions) { + for (Parameter param : callable.getParameters()) { + List> generated = inferContext(param.getType()); + // Find the generated param type, if any + for (UnsolvedSymbolAlternates symbol : generated) { + if (symbol instanceof UnsolvedClassOrInterfaceAlternates type) { + if (type.getClassName().equals(JavaParserUtil.erase(param.getTypeAsString()))) { + for (UnsolvedClassOrInterface alt : type.getAlternates()) { + alt.addMustPreserveNode(callable); + } + break; + } + } + } + } + } + } + + // A collection of sets of fqns. Each set represents potentially a different class/interface. + Collection> potentialScopeFQNs = + fullyQualifiedNameGenerator.getFQNsForExpressionLocation(methodCall); + + // Special case: handle this/super separately since potentialScopeFQNs + // provides more information than a this/super expression alone in + // inferContextImpl + if (methodCall.hasScope()) { + if (methodCall.getScope().get().isThisExpr() || methodCall.getScope().get().isSuperExpr()) { + handleThisOrSuperExpr(potentialScopeFQNs); + } else { + // Generate everything in the scopes before + inferContextImpl(methodCall.getScope().get(), result); + } + } + // If there are no methods that match this in the type or its ancestors, we need to generate it. + else if (definitions.isEmpty()) { + handleThisOrSuperExpr(potentialScopeFQNs); + } + + // Could be empty if the method is called on a NameExpr with a union type, + // but the method is located in a known class. + + // potentialScopeFQNs may also be size 1 if it is unresolvable due to its location in a + // lambda body. The second part of the condition checks for this edge case, where the method + // may be known. + if (potentialScopeFQNs.isEmpty() + || (potentialScopeFQNs.size() == 1 + && doesOverlapWithKnownType(potentialScopeFQNs.iterator().next()))) { + return; + } + + Map> argumentToParameterPotentialFQNs = new HashMap<>(); + + Set potentialFQNs = + fullyQualifiedNameGenerator.generateMethodFQNsWithSideEffect( + methodCall, potentialScopeFQNs, argumentToParameterPotentialFQNs, true); + boolean hasNullInSignature = + argumentToParameterPotentialFQNs.keySet().stream().anyMatch(Expression::isNullLiteralExpr); + + if (hasNullInSignature) { + Set scopesFlattened = + potentialScopeFQNs.stream().flatMap(Set::stream).collect(Collectors.toSet()); + + // If we see null, try to find an existing generated method which has an object instead + for (String fqn : generatedSymbols.keySet()) { + UnsolvedSymbolAlternates gen = generatedSymbols.get(fqn); + if (gen instanceof UnsolvedMethodAlternates) { + String qualifiedMethodName = fqn.substring(0, fqn.indexOf('(')); + String methodName = + qualifiedMethodName.substring(qualifiedMethodName.lastIndexOf('#') + 1); + String className = qualifiedMethodName.substring(0, qualifiedMethodName.lastIndexOf('#')); + + if (!methodName.equals(methodCall.getNameAsString()) + || !scopesFlattened.contains(className)) { + continue; + } + + String[] parameterList = + fqn.substring(fqn.indexOf('(') + 1).replace(")", "").split(",\\s*", -1); + + if (parameterList.length != methodCall.getArguments().size()) { + continue; + } + + boolean valid = true; + for (int i = 0; i < parameterList.length; i++) { + String parameter = parameterList[i]; + if (parameter.trim().equals("null")) { + valid = false; + break; + } + + if (methodCall.getArgument(i).isNullLiteralExpr() + && JavaLangUtils.isPrimitive(parameter)) { + valid = false; + break; + } + + Set fqns = + argumentToParameterPotentialFQNs.get(methodCall.getArgument(i)); + if (fqns == null + || !fqns.stream() + .anyMatch( + fqnSet -> fqnSet.erasedFqns().contains(JavaParserUtil.erase(parameter)))) { + valid = false; + break; + } + } + + if (valid) { + // If any exists, we don't have to create any method + return; + } + } + } + } + + UnsolvedSymbolAlternates generated = findExistingAndUpdateFQNs(potentialFQNs); + + // TODO: see if this is an issue if two different methods have the same parameter type + Map> returnTypeToMustPreserveNode = + getTypeToCallableDeclarationFromArgument(methodCall); + + UnsolvedMethodAlternates generatedMethod; + + if (generated instanceof UnsolvedMethodAlternates) { + generatedMethod = (UnsolvedMethodAlternates) generated; + + if (!returnTypeToMustPreserveNode.isEmpty()) { + generatedMethod.updateReturnTypesAndMustPreserveNodes(returnTypeToMustPreserveNode); + } + } else { + List potentialParents = new ArrayList<>(); + for (Set set : potentialScopeFQNs) { + if (doesOverlapWithKnownType(set)) { + return; + } + + UnsolvedSymbolAlternates gen = findExistingAndUpdateFQNs(set); + + if (gen == null) { + throw new RuntimeException( + "Method scope types are not yet created: " + methodCall + " with scope " + set); + } + potentialParents.add((UnsolvedClassOrInterfaceAlternates) gen); + } + + for (Expression argument : methodCall.getArguments()) { + inferContextImpl(argument, result); + } + + List> parametersToMustPreserve = + generateParameterToMustPreserveMap( + methodCall.getArguments(), argumentToParameterPotentialFQNs); + + if (returnTypeToMustPreserveNode.isEmpty()) { + MethodDeclaration declarationInThisTypeWithSameSignature = + JavaParserUtil.tryFindMethodDeclarationWithSameSignatureFromThisType( + methodCall, fqnsToCompilationUnits); + + Set returnTypes = new LinkedHashSet<>(); + if (declarationInThisTypeWithSameSignature != null) { + returnTypes.add( + getOrCreateMemberTypeFromFQNs( + fullyQualifiedNameGenerator.getFQNsFromType( + declarationInThisTypeWithSameSignature.getType()))); + } else { + for (FullyQualifiedNameSet fqns : + fullyQualifiedNameGenerator.getFQNsForExpressionType(methodCall)) { + returnTypes.add(getOrCreateMemberTypeFromFQNs(fqns)); + } + } + + generatedMethod = + UnsolvedMethodAlternates.createWithPreservation( + methodCall.getNameAsString(), + returnTypes, + potentialParents, + parametersToMustPreserve, + List.of()); + } else { + generatedMethod = + UnsolvedMethodAlternates.createWithPreservation( + methodCall.getNameAsString(), + returnTypeToMustPreserveNode, + potentialParents, + parametersToMustPreserve, + List.of()); + } + + if (hasNullInSignature) { + methodsWithNullInSignature.add(generatedMethod); + } else if (!methodsWithNullInSignature.isEmpty()) { + Set scopesFlattened = + potentialScopeFQNs.stream().flatMap(Set::stream).collect(Collectors.toSet()); + + UnsolvedMethodAlternates toRemove = null; + for (UnsolvedMethodAlternates method : methodsWithNullInSignature) { + for (String fqn : method.getFullyQualifiedNames()) { + String qualifiedMethodName = fqn.substring(0, fqn.indexOf('(')); + String methodName = + qualifiedMethodName.substring(qualifiedMethodName.lastIndexOf('#') + 1); + String className = + qualifiedMethodName.substring(0, qualifiedMethodName.lastIndexOf('#')); + + if (!methodName.equals(methodCall.getNameAsString()) + || !scopesFlattened.contains(className)) { + continue; + } + + String[] parameterList = + fqn.substring(fqn.indexOf('(') + 1).replace(")", "").split(",\\s*", -1); + + if (parameterList.length != methodCall.getArguments().size()) { + continue; + } + + boolean valid = true; + for (int i = 0; i < parameterList.length; i++) { + String parameter = parameterList[i]; + Expression arg = methodCall.getArgument(i); + + Set fqns = argumentToParameterPotentialFQNs.get(arg); + + if (fqns == null) { + valid = false; + break; + } + + Set argumentFQNsFlattened = + fqns.stream() + .flatMap(fqnSet -> fqnSet.erasedFqns().stream()) + .collect(Collectors.toSet()); + + if (parameter.equals("null")) { + if (argumentFQNsFlattened.stream().anyMatch(JavaLangUtils::isPrimitive)) { + valid = false; + break; + } + continue; + } + + if (!argumentFQNsFlattened.contains(JavaParserUtil.erase(parameter))) { + valid = false; + break; + } + } + + if (valid) { + toRemove = method; + break; + } + } + + if (toRemove != null) { + break; + } + } + + if (toRemove != null) { + methodsWithNullInSignature.remove(toRemove); + removeSymbolFromGeneratedSymbolsMap(toRemove); + } + } + addNewSymbolToGeneratedSymbolsMap(generatedMethod); + + if (methodCall.getTypeArguments().isPresent()) { + generatedMethod.setNumberOfTypeVariables(methodCall.getTypeArguments().get().size()); + } + } + + if (JavaParserUtil.getFQNIfStaticMember(methodCall) != null) { + generatedMethod.setStatic(); + } + + if (!hasNullInSignature) { + // Never add a method with a null parameter + result.add(generatedMethod); + } + } + + /** + * Helper method for {@link #inferContextImpl(Node, List)}. Adds the existing definition to the + * result if found, or a new definition if one does not already exist. Handles both explicit + * constructor invocation statements (super/this) and constructor calls (new ...()). + * + * @param location The location of the constructor + * @param constructorName The name of the constructor + * @param arguments The arguments of the constructor call + * @param numberOfTypeParams The number of type parameters of the constructor only + * @param result The result of inferContext + */ + private void handleConstructorCall( + UnsolvedClassOrInterfaceAlternates location, + String constructorName, + List arguments, + int numberOfTypeParams, + List> result) { + Map> argumentToParameterPotentialFQNs = new HashMap<>(); + List> simpleNames = new ArrayList<>(); + + for (Expression argument : arguments) { + Set fqns = + fullyQualifiedNameGenerator.getFQNsForExpressionType(argument); + Set simpleNamesForThisArgumentType = new LinkedHashSet<>(); + for (FullyQualifiedNameSet fqnSet : fqns) { + String first = fqnSet.erasedFqns().iterator().next(); + simpleNamesForThisArgumentType.add(JavaParserUtil.getSimpleNameFromQualifiedName(first)); + } + simpleNames.add(simpleNamesForThisArgumentType); + argumentToParameterPotentialFQNs.put(argument, fqns); + } + + Set potentialFQNs = new LinkedHashSet<>(); + + for (List simpleNamesCombo : JavaParserUtil.generateAllCombinations(simpleNames)) { + for (String potentialScopeFQN : location.getFullyQualifiedNames()) { + potentialFQNs.add( + potentialScopeFQN + + "#" + + constructorName + + "(" + + String.join(", ", simpleNamesCombo) + + ")"); + } + } + + UnsolvedSymbolAlternates generated = findExistingAndUpdateFQNs(potentialFQNs); + UnsolvedMethodAlternates generatedMethod; + + if (generated instanceof UnsolvedMethodAlternates) { + generatedMethod = (UnsolvedMethodAlternates) generated; + } else { + for (Expression argument : arguments) { + inferContextImpl(argument, result); + } + + List> parametersToMustPreserve = + generateParameterToMustPreserveMap(arguments, argumentToParameterPotentialFQNs); + + generatedMethod = + UnsolvedMethodAlternates.createWithPreservation( + constructorName, + Set.of(new SolvedMemberType("")), + List.of(location), + parametersToMustPreserve, + List.of()); + + addNewSymbolToGeneratedSymbolsMap(generatedMethod); + + generatedMethod.setNumberOfTypeVariables(numberOfTypeParams); + + result.add(generatedMethod); + } + } + + /** + * Given a list of argument expressions (from a method call, constructor call) and a map of + * arguments to potential FQN sets, return a list of maps, each representing mutually exclusive + * parameter types to nodes that must be preserved if that parameter type is used. + * + * @param args The collection of argument expressions + * @param argumentToParameterPotentialFQNs A map from each argument expression to its potential + * fully qualified name sets + * @return A list of maps, each representing mutually exclusive parameter types to nodes that must + * be preserved + */ + private List> generateParameterToMustPreserveMap( + Collection args, + Map> argumentToParameterPotentialFQNs) { + List> parametersToMustPreserve = new ArrayList<>(); + + for (Expression argument : args) { + Set potentialParameterTypes = + argumentToParameterPotentialFQNs.get(argument); + + // This null check is just to satisfy the error checker + if (potentialParameterTypes == null) { + throw new RuntimeException("Expected non-null when this is null"); + } + + Map potentialParameterToMustPreserveNode = new HashMap<>(); + if (argument.isMethodReferenceExpr()) { + List resolved = + JavaParserUtil.getMethodDeclarationsFromMethodRef(argument.asMethodReferenceExpr()); + + for (ResolvedMethodLikeDeclaration method : resolved) { + CallableDeclaration ast = + (CallableDeclaration) + JavaParserUtil.tryFindAttachedNode(method, fqnsToCompilationUnits); + + if (ast == null) { + continue; + } + + potentialParameterToMustPreserveNode.put( + fullyQualifiedNameGenerator.getFunctionalInterfaceForResolvedMethod( + argument.asMethodReferenceExpr(), method), + ast); + } + } + + for (FullyQualifiedNameSet potentialParameterType : potentialParameterTypes) { + parametersToMustPreserve.add( + Collections.singletonMap( + getOrCreateMemberTypeFromFQNs(potentialParameterType), + potentialParameterToMustPreserveNode.get(potentialParameterType))); + } + } + + return parametersToMustPreserve; + } + + /** + * Helper method for {@link #inferContextImpl(Node, List)}. Given an existing method declaration + * with {@code @Override}, generates a synthetic method with the same parameter and return types + * with potential declaring types in all unsolvable ancestors. + * + * @param methodDecl The method declaration to process + * @param result The result list to add generated symbols to + */ + private void handleMethodDeclarationWithOverride( + MethodDeclaration methodDecl, List> result) { + Collection> potentialScopeFQNs; + if (methodDecl.getParentNode().orElse(null) instanceof ObjectCreationExpr anonClass) { + try { + ResolvedType resolvedType = anonClass.getType().resolve(); + + TypeDeclaration parentClass = + JavaParserUtil.getTypeFromQualifiedName( + resolvedType.describe(), fqnsToCompilationUnits); + + if (parentClass == null) { + return; + } + + potentialScopeFQNs = + fullyQualifiedNameGenerator + .getFQNsOfAllUnresolvableParents(parentClass, methodDecl) + .values(); + } catch (UnsolvedSymbolException ex) { + potentialScopeFQNs = + Set.of(fullyQualifiedNameGenerator.getFQNsFromType(anonClass.getType()).erasedFqns()); + } + } else { + potentialScopeFQNs = + fullyQualifiedNameGenerator + .getFQNsOfAllUnresolvableParents( + JavaParserUtil.getEnclosingClassLike(methodDecl), methodDecl) + .values(); + } + + if (potentialScopeFQNs.isEmpty()) { + // If there are no potential scope FQNs, then this method is likely an override of a method + // in an existing class or JDK interface + return; + } + + String simpleSignature = methodDecl.getNameAsString() + "("; + + for (Parameter param : methodDecl.getParameters()) { + simpleSignature += + JavaParserUtil.getSimpleNameFromQualifiedName( + JavaParserUtil.erase(param.getTypeAsString())); + } + + simpleSignature += ")"; + + Set potentialMethodFQNs = new LinkedHashSet<>(); + for (Set set : potentialScopeFQNs) { + for (String fqn : set) { + potentialMethodFQNs.add(fqn + "#" + simpleSignature); + } + } + + UnsolvedMethodAlternates generated = + (UnsolvedMethodAlternates) findExistingAndUpdateFQNs(potentialMethodFQNs); + + if (generated != null) { + return; + } + + List parameters = new ArrayList<>(); + for (Parameter param : methodDecl.getParameters()) { + MemberType paramType = + getOrCreateMemberTypeFromFQNs( + fullyQualifiedNameGenerator.getFQNsFromType(param.getType())); + parameters.add(paramType); + } + + List potentialDeclaringTypes = new ArrayList<>(); + + for (Set fqns : potentialScopeFQNs) { + potentialDeclaringTypes.add(findExistingAndUpdateFQNsOrCreateNewType(fqns)); + } + + MemberType returnType = + getOrCreateMemberTypeFromFQNs( + fullyQualifiedNameGenerator.getFQNsFromType(methodDecl.getType())); + + List exceptions = new ArrayList<>(); + for (ReferenceType exception : methodDecl.getThrownExceptions()) { + MemberType exceptionType = + getOrCreateMemberTypeFromFQNs(fullyQualifiedNameGenerator.getFQNsFromType(exception)); + exceptions.add(exceptionType); + } + + AccessSpecifier specifier = methodDecl.getAccessSpecifier(); + String accessModifier = + switch (specifier) { + case PUBLIC -> "public"; + case PROTECTED -> "protected"; + case PRIVATE -> throw new RuntimeException("Cannot override with a private method."); + case NONE -> ""; + }; + + generated = + UnsolvedMethodAlternates.create( + methodDecl.getNameAsString(), + Set.of(returnType), + potentialDeclaringTypes, + parameters.stream().map(p -> Set.of(p)).toList(), + exceptions, + accessModifier); + + addNewSymbolToGeneratedSymbolsMap(generated); + result.add(generated); + } + + /** + * Helper method for {@link #inferContextImpl(Node, List)}. Generates the method corresponding + * with the given method reference expression (parameterless void). If the method reference + * matches a method in java.lang.Object, no new method is generated. Likewise, if a method with + * the same qualified name (not signature) is already generated, no new method is created. In + * other cases, a new, parameterless void method is generated and added to the result. + * + * @param methodRef The method reference expression + * @param result The result of inferContext + */ + private void handleMethodReferenceExpr( + MethodReferenceExpr methodRef, List> result) { + if (JavaLangUtils.getJavaLangObjectMethods().keySet().stream() + .anyMatch(k -> k.startsWith(methodRef.getIdentifier()))) { + // If the method reference matches a method in java.lang.Object, we can use that method + // directly without generating a new one. + return; + } + + boolean needToGenerateMethod = + fullyQualifiedNameGenerator.getExpressionTypesIfRepresentsGenerated(methodRef) == null + && JavaParserUtil.getMethodDeclarationsFromMethodRef(methodRef).isEmpty(); + + String methodName = JavaParserUtil.erase(methodRef.getIdentifier()); + boolean isConstructor = false; + + if (methodName.equals("new")) { + methodName = + JavaParserUtil.getSimpleNameFromQualifiedName( + JavaParserUtil.erase(methodRef.getScope().toString())); + isConstructor = true; + } + + List scope = new ArrayList<>(); + Collection> potentialScopeFQNs; + Set scopeFQNsFlattened; + + if (needToGenerateMethod) { + inferContextImpl(methodRef.getScope(), result); + + potentialScopeFQNs = fullyQualifiedNameGenerator.getFQNsForExpressionLocation(methodRef); + scopeFQNsFlattened = + potentialScopeFQNs.stream().flatMap(Set::stream).collect(Collectors.toSet()); + + for (Set set : potentialScopeFQNs) { + UnsolvedClassOrInterfaceAlternates classOrInterface = + (UnsolvedClassOrInterfaceAlternates) findExistingAndUpdateFQNs(set); + + if (classOrInterface == null) { + throw new RuntimeException( + "Type is not generated for method reference scope: " + + methodRef + + " with FQNs " + + set); + } + + scope.add(classOrInterface); + } + } else { + potentialScopeFQNs = Set.of(); + scopeFQNsFlattened = Set.of(); + } + + for (FullyQualifiedNameSet functionalInterface : + fullyQualifiedNameGenerator.getFQNsForExpressionType(methodRef)) { + FullyQualifiedNameSet normalized = + FunctionalInterfaceHelper.convertToNormalFunctionalInterface(functionalInterface); + + List parameters = new ArrayList<>(normalized.typeArguments()); + FullyQualifiedNameSet returnTypeFromTypeArgs; + + String funcIntName = + JavaParserUtil.getSimpleNameFromQualifiedName(normalized.erasedFqns().iterator().next()); + + if (funcIntName.contains("SyntheticConsumer")) { + returnTypeFromTypeArgs = null; + } else if (funcIntName.contains("SyntheticFunction")) { + returnTypeFromTypeArgs = + normalized.typeArguments().get(normalized.typeArguments().size() - 1); + } else { + returnTypeFromTypeArgs = + FunctionalInterfaceHelper.getReturnTypeFromNormalizedFunctionalInterface(normalized); + } + + MemberType returnType; + + boolean isVoid = false; + if (isConstructor) { + if (returnTypeFromTypeArgs != null) { + parameters.remove(parameters.size() - 1); + } + + returnType = new SolvedMemberType(""); + } else { + if (returnTypeFromTypeArgs != null) { + parameters.remove(parameters.size() - 1); + + // Get rid of the wildcard + FullyQualifiedNameSet unwildcarded = returnTypeFromTypeArgs; + + if (returnTypeFromTypeArgs.wildcard() != null) { + if (returnTypeFromTypeArgs.equals(FullyQualifiedNameSet.UNBOUNDED_WILDCARD)) { + unwildcarded = new FullyQualifiedNameSet("java.lang.Object"); + } else { + unwildcarded = + new FullyQualifiedNameSet( + returnTypeFromTypeArgs.erasedFqns(), returnTypeFromTypeArgs.typeArguments()); + } + } + + returnType = getOrCreateMemberTypeFromFQNs(unwildcarded); + } else { + isVoid = true; + returnType = new SolvedMemberType("void"); + } + } + + boolean isStatic = false; + + if (methodRef.getScope().isTypeExpr()) { + if (parameters.isEmpty()) { + isStatic = true; + } else { + FullyQualifiedNameSet param1 = parameters.get(0); + + if (param1.erasedFqns().stream().anyMatch(scopeFQNsFlattened::contains)) { + parameters.remove(0); + } else { + isStatic = true; + } + } + } + + result.addAll( + generateFunctionalInterface(normalized.erasedFqns(), parameters.size(), isVoid)); + + if (!needToGenerateMethod) { + continue; + } + + List simpleNames = new ArrayList<>(); + List> parametersAsMemberType = new ArrayList<>(); + + for (FullyQualifiedNameSet param : parameters) { + String simpleName = + JavaParserUtil.getSimpleNameFromQualifiedName(param.erasedFqns().iterator().next()); + simpleNames.add(simpleName); + parametersAsMemberType.add(Set.of(getOrCreateMemberTypeFromFQNs(param))); + } + + Set potentialFQNs = new LinkedHashSet<>(); + + for (Set set : potentialScopeFQNs) { + for (String potentialScopeFQN : set) { + potentialFQNs.add( + potentialScopeFQN + "#" + methodName + "(" + String.join(", ", simpleNames) + ")"); + } + } + + UnsolvedSymbolAlternates generated = findExistingAndUpdateFQNs(potentialFQNs); + + if (generated == null) { + UnsolvedMethodAlternates generatedMethod = + UnsolvedMethodAlternates.create( + methodName, Set.of(returnType), scope, parametersAsMemberType); + + if (isStatic) { + generatedMethod.setStatic(); + } + + if (methodRef.getTypeArguments().isPresent()) { + generatedMethod.setNumberOfTypeVariables(methodRef.getTypeArguments().get().size()); + } + + addNewSymbolToGeneratedSymbolsMap(generatedMethod); + + result.add(generatedMethod); + } + } + } + + /** + * Helper method for {@link #inferContextImpl(Node, List)}. Generates a functional interface for + * the lambda (if a built-in one cannot be used) and adds it to {@code result}. + * + * @param lambda The lambda expression + * @param result The result of inferContext + */ + private void handleLambdaExpr(LambdaExpr lambda, List> result) { + boolean isVoid; + if (lambda.getExpressionBody().isPresent()) { + Expression body = lambda.getExpressionBody().get(); + Set fqns = fullyQualifiedNameGenerator.getFQNsForExpressionType(body); + isVoid = + fqns.size() == 1 + && fqns.iterator().next().erasedFqns().size() == 1 + && fqns.iterator().next().erasedFqns().iterator().next().equals("void"); + } else { + isVoid = + !lambda.getBody().asBlockStmt().getStatements().stream() + .anyMatch(stmt -> stmt instanceof ReturnStmt); + } + + int arity = lambda.getParameters().size(); + + // Lambdas will always only have one type + FullyQualifiedNameSet potentialFQNs = + fullyQualifiedNameGenerator.getFQNsForExpressionType(lambda).iterator().next(); + + for (String unerased : potentialFQNs.erasedFqns()) { + if (unerased.startsWith("java.")) { + // Built-in functional interface can be used; no need for synthetic generation. + return; + } + } + + result.addAll(generateFunctionalInterface(potentialFQNs.erasedFqns(), arity, isVoid)); + } + + /** + * Creates a new functional interface and its method. Returns generated symbols as a list; if none + * needed to be generated, then returns an empty list. + * + * @param fqns The set of erased fqns representing this functional interface + * @param arity The number of parameters + * @param isVoid Whether the functional interface's method returns void + * @return A list of generated symbols, or an empty list if none were generated + */ + private List> generateFunctionalInterface( + Set fqns, int arity, boolean isVoid) { + if (doesOverlapWithKnownType(fqns)) { + return Collections.emptyList(); + } + + UnsolvedClassOrInterfaceAlternates functionalInterface = + findExistingAndUpdateFQNsOrCreateNewType(fqns); + functionalInterface.setTypeVariables(arity + (isVoid ? 0 : 1)); + functionalInterface.setType(UnsolvedClassOrInterfaceType.INTERFACE); + functionalInterface.addAnnotation("@FunctionalInterface"); + + List paramList = functionalInterface.getTypeVariables(); + List> params = new ArrayList<>(); + + // remove the last element of params, because that's the return type, not a parameter + for (int i = 0; i < paramList.size() - (isVoid ? 0 : 1); i++) { + params.add(Set.of(new SolvedMemberType(paramList.get(i)))); + } + + String paramListAsString = String.join(", ", paramList); + if (!isVoid) { + int lastIndexOfComma = paramListAsString.lastIndexOf(','); + if (lastIndexOfComma != -1) { + paramListAsString = paramListAsString.substring(0, lastIndexOfComma); + } else { + paramListAsString = ""; + } + } + + Set potentialMethodFQNs = new LinkedHashSet<>(); + + for (String fqn : fqns) { + potentialMethodFQNs.add(fqn + "#apply(" + paramListAsString + ")"); + } + + if (findExistingAndUpdateFQNs(potentialMethodFQNs) != null) { + // If the method already exists, no need to create a new one + return List.of(functionalInterface); + } + + String returnType = isVoid ? "void" : "T" + arity; + UnsolvedMethodAlternates apply = + UnsolvedMethodAlternates.create( + "apply", + Set.of(new SolvedMemberType(returnType)), + List.of(functionalInterface), + params); + + addNewSymbolToGeneratedSymbolsMap(apply); + + return List.of(functionalInterface, apply); + } + + /** + * After checking if an expression's scope is super/this, pass in the value collection of the + * result of {@link fullyQualifiedNameGenerator#getFQNsForExpressionLocation(Expression)} to this + * method to ensure all possible types are generated. + * + * @param fqnSets The value collection of the result of getFQNsForExpressionLocation, if the scope + * is super/this; a collection of FQN sets each representing a different type. + */ + private void handleThisOrSuperExpr(Collection> fqnSets) { + for (Set fqnSet : fqnSets) { + findExistingAndUpdateFQNsOrCreateNewType(fqnSet); + } + } + + /** + * Given a potential argument expression, this method returns a map of MemberType to + * CallableDeclaration. For example, if the argument is a method call expression, foo(), as an + * argument of another method call, bar(foo()), this method will return a map of potential return + * types of foo() (based on the definitions of bar with an arity of 1) to the CallableDeclaration + * of bar. This method also works if {@code argument} is a field expression, or if the parent node + * is a constructor/explicit constructor invocation. + * + * @param argument The argument expression to analyze + * @return A map of potential return types to their corresponding CallableDeclaration. Returns an + * empty map if no potential return types are found, or if the argument is not part of a + * solvable method/constructor call. + */ + private Map> getTypeToCallableDeclarationFromArgument( + Expression argument) { + // If this expression is an argument of a solvable method call, we have multiple potential field + // types to choose from, based on each definition + Node parent = argument.getParentNode().get(); + int paramNum = -1; + Map> returnTypeToMustPreserveNode = new LinkedHashMap<>(); + + if (!(parent instanceof NodeWithArguments withArgs)) { + return returnTypeToMustPreserveNode; + } + + for (int i = 0; i < withArgs.getArguments().size(); i++) { + if (withArgs.getArgument(i).equals(argument)) { + paramNum = i; + break; + } + } + + // paramNum could still be -1 if methodCall is the scope of another method call, + // not an argument + if (paramNum == -1) { + return returnTypeToMustPreserveNode; + } + + List> parentCallableDeclarations = + JavaParserUtil.tryResolveNodeWithUnresolvableArguments(withArgs, fqnsToCompilationUnits); + + for (CallableDeclaration callable : parentCallableDeclarations) { + Parameter param = callable.getParameter(paramNum); + + MemberType memberType = + getOrCreateMemberTypeFromFQNs( + fullyQualifiedNameGenerator.getFQNsFromType(param.getType())); + + returnTypeToMustPreserveNode.put(memberType, callable); + } + + return returnTypeToMustPreserveNode; + } + + /** + * Replaces all methods with null in their signature to use java.lang.Object instead, and returns + * the updated methods. + * + * @return The updated methods. + */ + public Set clearMethodsWithNull() { + for (UnsolvedMethodAlternates unsolvedMethodAlternates : methodsWithNullInSignature) { + for (UnsolvedMethod alternate : unsolvedMethodAlternates.getAlternates()) { + alternate.replaceParameterType( + new SolvedMemberType("null"), SolvedMemberType.JAVA_LANG_OBJECT); + } + + removeSymbolFromGeneratedSymbolsMap(unsolvedMethodAlternates); + addNewSymbolToGeneratedSymbolsMap(unsolvedMethodAlternates); + } + + Set result = Set.copyOf(methodsWithNullInSignature); + methodsWithNullInSignature.clear(); + return result; + } + + /** + * Call this method on each node to gather more information on potential unsolved symbols. Call + * this method AFTER all unsolved symbols are generated. + * + * @param node The node to gather more information from + * @return An object of type {@link UnsolvedGenerationResult}, usually empty, but the close() + * method(s) if first time confirmation of an AutoCloseable, or if the return type is updated + * in a method call expression. + */ + public UnsolvedGenerationResult addInformation(Node node) { + List> toAdd = new ArrayList<>(); + List> toRemove = new ArrayList<>(); + + if (node instanceof ClassOrInterfaceDeclaration decl) { + for (ClassOrInterfaceType implemented : decl.getImplementedTypes()) { + UnsolvedClassOrInterfaceAlternates syntheticType = + (UnsolvedClassOrInterfaceAlternates) + findExistingAndUpdateFQNs(fullyQualifiedNameGenerator.getFQNsFromType(implemented)); + + if (syntheticType != null) { + syntheticType.setType(UnsolvedClassOrInterfaceType.INTERFACE); + } + } + for (ClassOrInterfaceType extended : decl.getExtendedTypes()) { + UnsolvedClassOrInterfaceAlternates syntheticType = + (UnsolvedClassOrInterfaceAlternates) + findExistingAndUpdateFQNs(fullyQualifiedNameGenerator.getFQNsFromType(extended)); + + if (syntheticType != null) { + syntheticType.setType( + decl.isInterface() + ? UnsolvedClassOrInterfaceType.INTERFACE + : UnsolvedClassOrInterfaceType.CLASS); + } + } + } else if (node instanceof EnumDeclaration decl) { + for (ClassOrInterfaceType implemented : decl.getImplementedTypes()) { + UnsolvedClassOrInterfaceAlternates syntheticType = + (UnsolvedClassOrInterfaceAlternates) + findExistingAndUpdateFQNs(fullyQualifiedNameGenerator.getFQNsFromType(implemented)); + + if (syntheticType != null) { + syntheticType.setType(UnsolvedClassOrInterfaceType.INTERFACE); + } + } + } else if (node instanceof MethodDeclaration methodDecl) { + for (ReferenceType thrownException : methodDecl.getThrownExceptions()) { + if (!thrownException.isClassOrInterfaceType()) { + continue; + } + + UnsolvedClassOrInterfaceAlternates syntheticType = + (UnsolvedClassOrInterfaceAlternates) + findExistingAndUpdateFQNs( + fullyQualifiedNameGenerator.getFQNsFromType( + thrownException.asClassOrInterfaceType())); + + // Method declaration throws clauses could be either checked or unchecked, but are typically + // checked exceptions. We'll force checked exceptions (java.lang.Exception) to be first so + // best-effort generates this as the alternate. + if (syntheticType != null + && !syntheticType.doesExtend(SolvedMemberType.JAVA_LANG_EXCEPTION)) { + // Remove java.lang.Error in case it was added as part of a throw statement (we want + // the alternate with Exception to generate first) + syntheticType.removeSuperClass(SolvedMemberType.JAVA_LANG_ERROR); + syntheticType.forceSuperClass(SolvedMemberType.JAVA_LANG_EXCEPTION); + syntheticType.forceSuperClass(SolvedMemberType.JAVA_LANG_ERROR); + toRemove.addAll(handleExtendThrowable(syntheticType)); + } + } + } else if (node instanceof ThrowStmt throwStmt) { + for (FullyQualifiedNameSet fqnSet : + fullyQualifiedNameGenerator.getFQNsForExpressionType(throwStmt.getExpression())) { + UnsolvedClassOrInterfaceAlternates syntheticType = + (UnsolvedClassOrInterfaceAlternates) findExistingAndUpdateFQNs(fqnSet); + + // If we only see a throw statement, assume it's an unchecked exception until we encounter + // evidence otherwise (catch, throws clauses) + if (syntheticType != null && !syntheticType.hasExtends()) { + syntheticType.forceSuperClass(SolvedMemberType.JAVA_LANG_ERROR); + toRemove.addAll(handleExtendThrowable(syntheticType)); + } + } + } else if (node instanceof TryStmt tryStmt) { + // Could be null if it is a solved type + List<@Nullable UnsolvedClassOrInterfaceAlternates> types = new ArrayList<>(); + for (Expression resource : tryStmt.getResources()) { + // Java 7-8: try (InputStream i = new FileInputStream("file")) + // Java 9+: try (r) + // https://javadoc.io/doc/com.github.javaparser/javaparser-core/latest/com/github/javaparser/ast/stmt/TryStmt.html + if (resource instanceof VariableDeclarationExpr varDeclExpr) { + // Types of LHS and RHS must extend AutoCloseable + + // Guaranteed to be a class or interface type + UnsolvedClassOrInterfaceAlternates lhs = + (UnsolvedClassOrInterfaceAlternates) + findExistingAndUpdateFQNs( + fullyQualifiedNameGenerator.getFQNsFromType(varDeclExpr.getElementType())); + types.add(lhs); + + for (FullyQualifiedNameSet type : + fullyQualifiedNameGenerator.getFQNsForExpressionType( + varDeclExpr.getVariables().get(0).getInitializer().get())) { + UnsolvedClassOrInterfaceAlternates rhs = + (UnsolvedClassOrInterfaceAlternates) findExistingAndUpdateFQNs(type); + + if (rhs == null) { + throw new RuntimeException("Unresolved type for resource initializer: " + type); + } + + types.add(rhs); + } + + } else if (resource instanceof NameExpr || resource instanceof FieldAccessExpr) { + for (FullyQualifiedNameSet fqnSet : + fullyQualifiedNameGenerator.getFQNsForExpressionType((Expression) resource)) { + UnsolvedClassOrInterfaceAlternates type = + (UnsolvedClassOrInterfaceAlternates) findExistingAndUpdateFQNs(fqnSet); + + if (type == null) { + throw new RuntimeException("Unresolved type for resource initializer: " + fqnSet); + } + + types.add(type); + } + } + } + List<@Nullable UnsolvedClassOrInterfaceAlternates> exceptions = new ArrayList<>(); + for (CatchClause clause : tryStmt.getCatchClauses()) { + Parameter exception = clause.getParameter(); + + if (exception.getType().isClassOrInterfaceType()) { + UnsolvedClassOrInterfaceAlternates type = + (UnsolvedClassOrInterfaceAlternates) + findExistingAndUpdateFQNs( + fullyQualifiedNameGenerator.getFQNsFromType(exception.getType())); + exceptions.add(type); + } else if (exception.getType().isUnionType()) { + for (ReferenceType refType : exception.getType().asUnionType().getElements()) { + UnsolvedClassOrInterfaceAlternates type = + (UnsolvedClassOrInterfaceAlternates) + findExistingAndUpdateFQNs( + fullyQualifiedNameGenerator.getFQNsFromType( + (ClassOrInterfaceType) refType)); + exceptions.add(type); + } + } + } + + for (UnsolvedClassOrInterfaceAlternates exception : exceptions) { + if (exception == null || exception.doesExtend(SolvedMemberType.JAVA_LANG_EXCEPTION)) { + continue; + } + // Remove java.lang.Error in case it was added as part of a throw statement (we want + // the alternate with Exception to generate first) + exception.removeSuperClass(SolvedMemberType.JAVA_LANG_ERROR); + exception.forceSuperClass(SolvedMemberType.JAVA_LANG_EXCEPTION); + exception.forceSuperClass(SolvedMemberType.JAVA_LANG_ERROR); + toRemove.addAll(handleExtendThrowable(exception)); + } + + for (UnsolvedClassOrInterfaceAlternates type : types) { + MemberType autoCloseable = new SolvedMemberType("java.lang.AutoCloseable"); + if (type == null || type.doesImplement(autoCloseable)) { + continue; + } + + type.forceSuperInterface(autoCloseable); + + UnsolvedMethodAlternates unsolvedMethodAlternates = + UnsolvedMethodAlternates.create( + "close", + Set.of(new SolvedMemberType("void")), + List.of(type), + List.of(), + List.of(SolvedMemberType.JAVA_LANG_EXCEPTION)); + + addNewSymbolToGeneratedSymbolsMap(unsolvedMethodAlternates); + toAdd.add(unsolvedMethodAlternates); + } + } else if (node instanceof InstanceOfExpr instanceOf) { + // If we have x : X and x instanceof Y, then X must be a supertype + // of Y if X != Y. The JLS says (15.20.2): "If a cast of the RelationalExpression to the + // ReferenceType would be rejected as a compile-time error, then the instanceof relational + // expression likewise produces a compile-time error. In such a situation, the result of the + // instanceof expression could never be true." + // + // This method uses this fact to add extends clauses to synthetic classes. + Type type; + if (instanceOf.getPattern().isPresent()) { + PatternExpr patternExpr = instanceOf.getPattern().get(); + type = patternExpr.getType(); + } else { + type = instanceOf.getType(); + } + + try { + type.resolve(); + return UnsolvedGenerationResult.EMPTY; + } catch (UnsolvedSymbolException e) { + // continue + } + + Expression relationalExpr = instanceOf.getExpression(); + + Set relational = + getMemberTypesAndExpectNonNullFromFQNSets( + fullyQualifiedNameGenerator.getFQNsForExpressionType(relationalExpr)); + + if (relational.isEmpty()) { + throw new RuntimeException( + "Unsolved relational expression when all unsolved symbols should be generated."); + } + + UnsolvedClassOrInterfaceAlternates referenceType = + (UnsolvedClassOrInterfaceAlternates) + findExistingAndUpdateFQNs(fullyQualifiedNameGenerator.getFQNsFromType(type)); + + if (referenceType == null) { + throw new RuntimeException( + "Unsolved instanceof type when all unsolved symbols should be generated: " + type); + } + + referenceType.addSuperType(relational); + } + + // This condition checks to see if the return type of a synthetic method definition + // can be updated by potential child classes. + // See VoidReturnDoubleTest for an example of why this is necessary + else if (node instanceof MethodCallExpr methodCall) { + matchMethodReturnTypesToKnownChildClasses(methodCall); + } else if (node instanceof TypeParameter typeParam) { + // All bounds after the first in a type parameter must be interfaces + // https://docs.oracle.com/javase/tutorial/java/generics/bounded.html + List elements = typeParam.getTypeBound(); + for (int i = 1; i < elements.size(); i++) { + UnsolvedClassOrInterfaceAlternates syntheticType = + (UnsolvedClassOrInterfaceAlternates) + findExistingAndUpdateFQNs( + fullyQualifiedNameGenerator.getFQNsFromType(elements.get(i))); + + if (syntheticType != null) { + syntheticType.setType(UnsolvedClassOrInterfaceType.INTERFACE); + } + } + } + + // Get super classes: type of LHS is a super type of the type of the RHS + if (node instanceof AssignExpr + || (node instanceof VariableDeclarator varDecl && varDecl.getInitializer().isPresent()) + || (node instanceof ReturnStmt returnStmt && returnStmt.getExpression().isPresent()) + || node instanceof LambdaExpr) { + Set lhsType; + Set rhsType; + + Supplier getResolvedTypeOfLHS; + + if (node instanceof AssignExpr assignExpr) { + Expression lhs = assignExpr.getTarget(); + Expression rhs = assignExpr.getValue(); + lhsType = + getMemberTypesAndExpectNonNullFromFQNSets( + fullyQualifiedNameGenerator.getFQNsForExpressionType(lhs)); + rhsType = + getMemberTypesAndExpectNonNullFromFQNSets( + fullyQualifiedNameGenerator.getFQNsForExpressionType(rhs)); + + getResolvedTypeOfLHS = () -> lhs.calculateResolvedType(); + } else if (node instanceof VariableDeclarator varDecl) { + Type lhs = varDecl.getType(); + + if (lhs.isVarType()) { + return UnsolvedGenerationResult.EMPTY; + } + + Expression rhs = varDecl.getInitializer().get(); + MemberType lhsMemberType = + getMemberTypeFromFQNs(fullyQualifiedNameGenerator.getFQNsFromType(lhs), false); + lhsType = lhsMemberType == null ? Set.of() : Set.of(lhsMemberType); + rhsType = + getMemberTypesAndExpectNonNullFromFQNSets( + fullyQualifiedNameGenerator.getFQNsForExpressionType(rhs)); + + getResolvedTypeOfLHS = () -> lhs.resolve(); + } else if (node instanceof ReturnStmt returnStmt) { + Node methodOrLambda = JavaParserUtil.findClosestMethodOrLambdaAncestor(returnStmt); + + if (methodOrLambda instanceof MethodDeclaration methodDecl) { + Type lhs = methodDecl.getType(); + Expression rhs = returnStmt.getExpression().get(); + MemberType lhsMemberType = + getMemberTypeFromFQNs(fullyQualifiedNameGenerator.getFQNsFromType(lhs), false); + lhsType = lhsMemberType == null ? Set.of() : Set.of(lhsMemberType); + rhsType = + getMemberTypesAndExpectNonNullFromFQNSets( + fullyQualifiedNameGenerator.getFQNsForExpressionType(rhs)); + getResolvedTypeOfLHS = () -> lhs.resolve(); + } else { + // Do not handle here: handle when we encounter the ancestor LambdaExpr node + return UnsolvedGenerationResult.EMPTY; + } + } else if (node instanceof LambdaExpr lambdaExpr) { + // See if the lambda expression type is available. If not, we can't get a relationship + + ResolvedType solvableTypeFromLambda; + try { + ResolvedType functionalInterface = lambdaExpr.calculateResolvedType(); + + if (!functionalInterface.isReferenceType() + || !functionalInterface.asReferenceType().getTypeDeclaration().isPresent()) { + return UnsolvedGenerationResult.EMPTY; + } + + ResolvedReferenceTypeDeclaration functionalInterfaceDecl = + functionalInterface.asReferenceType().getTypeDeclaration().get(); + + if (!functionalInterfaceDecl.isFunctionalInterface()) { + return UnsolvedGenerationResult.EMPTY; + } + + solvableTypeFromLambda = + functionalInterfaceDecl.getAllMethods().iterator().next().returnType(); + } catch (UnsolvedSymbolException ex) { + return UnsolvedGenerationResult.EMPTY; + } + + if (lambdaExpr.getExpressionBody().isPresent()) { + MemberType lhsMemberType = + getMemberTypeFromFQNs( + fullyQualifiedNameGenerator.getFQNsForResolvedType(solvableTypeFromLambda), + false); + lhsType = lhsMemberType == null ? Set.of() : Set.of(lhsMemberType); + rhsType = + getMemberTypesAndExpectNonNullFromFQNSets( + fullyQualifiedNameGenerator.getFQNsForExpressionType( + lambdaExpr.getExpressionBody().get())); + getResolvedTypeOfLHS = () -> solvableTypeFromLambda; + } else { + ReturnStmt returnStmt = + (ReturnStmt) + lambdaExpr.getBody().asBlockStmt().stream() + .filter(n -> n instanceof ReturnStmt) + .findFirst() + .orElse(null); + + if (returnStmt == null || !returnStmt.getExpression().isPresent()) { + return UnsolvedGenerationResult.EMPTY; + } + + MemberType lhsMemberType = + getMemberTypeFromFQNs( + fullyQualifiedNameGenerator.getFQNsForResolvedType(solvableTypeFromLambda), + false); + lhsType = lhsMemberType == null ? Set.of() : Set.of(lhsMemberType); + rhsType = + getMemberTypesAndExpectNonNullFromFQNSets( + fullyQualifiedNameGenerator.getFQNsForExpressionType( + returnStmt.getExpression().get())); + getResolvedTypeOfLHS = () -> solvableTypeFromLambda; + } + } else { + throw new RuntimeException("Impossible error"); + } + + if (rhsType.isEmpty()) { + throw new RuntimeException("Type has not been generated for the RHS of " + node); + } + + if (lhsType.isEmpty()) { + throw new RuntimeException("Type has not been generated for the LHS of " + node); + } + + handleLHSAndRHSRelationship(lhsType, rhsType, getResolvedTypeOfLHS); + } else if (node instanceof MethodCallExpr + || node instanceof ObjectCreationExpr + || node instanceof ExplicitConstructorInvocationStmt + || (node instanceof EnumConstantDeclaration enumConstantDeclaration + && enumConstantDeclaration.getArguments().isNonEmpty())) { + NodeWithArguments nodeWithArgs = (NodeWithArguments) node; + + ResolvedMethodLikeDeclaration resolved; + try { + if (!(node instanceof EnumConstantDeclaration)) { + resolved = (ResolvedMethodLikeDeclaration) ((Resolvable) nodeWithArgs).resolve(); + + if (resolved == null) { + throw new RuntimeException( + "Resolved declaration is null when it shouldn't be: " + node); + } + } else { + resolved = null; + } + } catch (UnsolvedSymbolException ex) { + resolved = null; + } catch (UnsupportedOperationException ex) { + if (node instanceof MethodCallExpr methodCall) { + Object decl = + JavaParserUtil.tryFindCorrespondingDeclarationForConstraintQualifiedExpression( + methodCall); + + if (decl instanceof ResolvedMethodDeclaration methodDecl) { + resolved = methodDecl; + } else { + throw ex; + } + } else { + throw ex; + } + } + + if (resolved != null) { + CallableDeclaration asAst = + (CallableDeclaration) + JavaParserUtil.tryFindAttachedNode(resolved, fqnsToCompilationUnits); + + for (int i = 0; i < nodeWithArgs.getArguments().size(); i++) { + MemberType lhsType; + Set rhsType = + getMemberTypesAndExpectNonNullFromFQNSets( + fullyQualifiedNameGenerator.getFQNsForExpressionType( + nodeWithArgs.getArgument(i))); + + Supplier getResolvedTypeOfLHS; + + try { + ResolvedParameterDeclaration param; + + if (i >= resolved.getNumberOfParams()) { + // Varargs; get last param + param = resolved.getLastParam(); + } else { + param = resolved.getParam(i); + } + + lhsType = + getMemberTypeFromFQNs( + fullyQualifiedNameGenerator.getFQNsForResolvedType(param.getType()), false); + getResolvedTypeOfLHS = () -> param.getType(); + } catch (UnsolvedSymbolException ex) { + if (asAst == null) { + // asAst cannot be null here: if the parameter type is unresolvable, then it must be + // in the project because JDK parameters will always be resolvable + throw new RuntimeException("asAst cannot be null"); + } + + Type type = asAst.getParameter(i).getType(); + + lhsType = + getMemberTypeFromFQNs(fullyQualifiedNameGenerator.getFQNsFromType(type), false); + getResolvedTypeOfLHS = () -> type.resolve(); + } + + if (rhsType.isEmpty()) { + throw new RuntimeException( + "Type has not been generated for " + nodeWithArgs.getArgument(i)); + } + + if (lhsType == null) { + throw new RuntimeException( + "Type has not been generated for the LHS of parameter " + i + " of " + node); + } + + handleLHSAndRHSRelationship(Set.of(lhsType), rhsType, getResolvedTypeOfLHS); + } + } else { + List> withUnresolvableArgs = + JavaParserUtil.tryResolveNodeWithUnresolvableArguments( + nodeWithArgs, fqnsToCompilationUnits); + + if (withUnresolvableArgs.isEmpty()) { + UnsolvedMethodAlternates genMethod = null; + if (node instanceof MethodCallExpr methodCall) { + Collection> methodScope = + fullyQualifiedNameGenerator.getFQNsForExpressionLocation(methodCall); + + // Could be empty if the method is called on a NameExpr with a union type, + // but the method is located in a known class. + if (methodScope.isEmpty()) { + return UnsolvedGenerationResult.EMPTY; + } + + for (Set set : methodScope) { + if (doesOverlapWithKnownType(set)) { + return UnsolvedGenerationResult.EMPTY; + } + } + + Set methodFqns = + fullyQualifiedNameGenerator.generateMethodFQNsWithSideEffect( + methodCall, methodScope, null, false); + genMethod = (UnsolvedMethodAlternates) findExistingAndUpdateFQNs(methodFqns); + + // If there is a null, and the Object version is not findable, then another call to the + // same method exists, and we'll get the signature from there instead + if (genMethod == null + && (isMethodABuiltInThrowableMethod(methodScope, methodFqns) + || methodCall.getArguments().stream() + .anyMatch(Expression::isNullLiteralExpr))) { + return UnsolvedGenerationResult.EMPTY; + } + } else { + genMethod = + (UnsolvedMethodAlternates) + findExistingAndUpdateFQNs(getFQNsForUnsolvableConstructor(node)); + } + + if (genMethod == null) { + throw new RuntimeException("Method alternates for " + node + " could not be found"); + } + + for (int i = 0; i < nodeWithArgs.getArguments().size(); i++) { + final int iCopy = i; + Set lhsType = + genMethod.getAlternates().stream() + .map(alt -> alt.getParameterList().get(iCopy)) + .collect(Collectors.toCollection(LinkedHashSet::new)); + Set rhsType = + getMemberTypesAndExpectNonNullFromFQNSets( + fullyQualifiedNameGenerator.getFQNsForExpressionType( + nodeWithArgs.getArgument(i))); + + // If the method is a synthetic definition, there is no resolved type of the LHS + Supplier getResolvedTypeOfLHS = + () -> { + throw new UnsolvedSymbolException(""); + }; + + if (rhsType.isEmpty()) { + throw new RuntimeException( + "Type has not been generated for " + nodeWithArgs.getArgument(i)); + } + + if (lhsType.isEmpty()) { + throw new RuntimeException( + "Type has not been generated for the LHS of parameter " + i + " of " + node); + } + + handleLHSAndRHSRelationship(lhsType, rhsType, getResolvedTypeOfLHS); + } + } else { + for (int i = 0; i < nodeWithArgs.getArguments().size(); i++) { + final int iCopy = i; + Set lhsType = + withUnresolvableArgs.stream() + .map( + alt -> { + MemberType paramType = + getMemberTypeFromFQNs( + fullyQualifiedNameGenerator.getFQNsFromType( + alt.getParameter(iCopy).getType()), + false); + + if (paramType == null) { + throw new RuntimeException( + "Parameter type could not be resolved for " + + alt.getParameter(iCopy)); + } + + return paramType; + }) + .collect(Collectors.toCollection(LinkedHashSet::new)); + + Set rhsType = + getMemberTypesAndExpectNonNullFromFQNSets( + fullyQualifiedNameGenerator.getFQNsForExpressionType( + nodeWithArgs.getArgument(i))); + + // Unless there is only one LHS possibility, we cannot resolve the type + Supplier getResolvedTypeOfLHS; + + if (withUnresolvableArgs.size() == 1) { + getResolvedTypeOfLHS = + () -> withUnresolvableArgs.get(0).getParameter(iCopy).getType().resolve(); + } else { + getResolvedTypeOfLHS = + () -> { + throw new UnsolvedSymbolException(""); + }; + } + + if (rhsType.isEmpty()) { + throw new RuntimeException( + "Type has not been generated for " + nodeWithArgs.getArgument(i)); + } + + if (lhsType.isEmpty()) { + throw new RuntimeException( + "Type has not been generated for the LHS of parameter " + i + " of " + node); + } + + handleLHSAndRHSRelationship(lhsType, rhsType, getResolvedTypeOfLHS); + } + } + } + } + + return new UnsolvedGenerationResult(toAdd, toRemove); + } + + /** + * Given a method call expression, try to match its return types to known child classes if this + * method declaration behind the call matches all of these requirements: + * + *
    + *
  • The method has an unsolved super method declaration with a generated synthetic definition + *
  • There are known child classes of the unsolved declaring type of this method + *
+ * + * If all these requirements are matched, then we update the return type of the method declaration + * based on the known child class method override return type. We also remove all instances of the + * synthetic return type. If multiple child classes are found, then we will find the least upper + * bound of all these return types. + * + *

If any of these requirements are not matched, then we return early and nothing gets changed. + * + * @param methodCall The method call expression to analyze + */ + private void matchMethodReturnTypesToKnownChildClasses(MethodCallExpr methodCall) { + Collection> potentialScopeFQNs = null; + ResolvedMethodDeclaration resolvedMethod = null; + MethodDeclaration ast = null; + try { + resolvedMethod = methodCall.resolve(); + } catch (UnsolvedSymbolException ex) { + ast = + (MethodDeclaration) + JavaParserUtil.tryFindSingleCallableForNodeWithUnresolvableArguments( + methodCall, fqnsToCompilationUnits); + + if (ast == null) { + potentialScopeFQNs = fullyQualifiedNameGenerator.getFQNsForExpressionLocation(methodCall); + } + } catch (UnsupportedOperationException ex) { + resolvedMethod = + (ResolvedMethodDeclaration) + JavaParserUtil.tryFindCorrespondingDeclarationForConstraintQualifiedExpression( + methodCall); + } + + if (resolvedMethod != null) { + // Potential scope is all unsolvable ancestors + ast = + (MethodDeclaration) + JavaParserUtil.tryFindAttachedNode(resolvedMethod, fqnsToCompilationUnits); + if (ast == null) { + return; + } + } + + if (ast != null) { + List unsolvableAncestors = + JavaParserUtil.getAllUnsolvableAncestors( + JavaParserUtil.getEnclosingClassLike(ast), fqnsToCompilationUnits); + + if (unsolvableAncestors.isEmpty()) { + return; + } + + potentialScopeFQNs = new ArrayList<>(); + for (ClassOrInterfaceType ancestor : unsolvableAncestors) { + potentialScopeFQNs.add(fullyQualifiedNameGenerator.getFQNsFromType(ancestor).erasedFqns()); + } + } + + // Could be empty if the method is called on a NameExpr with a union type, + // but the method is located in a known class. + if (potentialScopeFQNs == null || potentialScopeFQNs.isEmpty()) { + return; + } + + for (Set set : potentialScopeFQNs) { + if (doesOverlapWithKnownType(set)) { + return; + } + } + + Set potentialFQNs = + fullyQualifiedNameGenerator.generateMethodFQNsWithSideEffect( + methodCall, potentialScopeFQNs, null, false); + + UnsolvedMethodAlternates alt = + (UnsolvedMethodAlternates) findExistingAndUpdateFQNs(potentialFQNs); + + if (alt == null) { + if (isMethodABuiltInThrowableMethod(potentialScopeFQNs, potentialFQNs) + || resolvedMethod != null + || ast != null) { + return; + } + + // If there is a null, and the Object version is not findable, then another call to the same + // method exists, and we'll get the signature from there instead + if (methodCall.getArguments().stream().anyMatch(Expression::isNullLiteralExpr)) { + return; + } + throw new RuntimeException( + "Unresolvable method is not generated when all unsolved symbols should be: " + + potentialFQNs); + } + + if (methodCall.hasScope()) { + Set potentialTypes = new LinkedHashSet<>(); + Expression scope = methodCall.getScope().get(); + try { + ResolvedValueDeclaration resolved; + if (scope.isFieldAccessExpr()) { + resolved = scope.asFieldAccessExpr().resolve(); + } else if (scope.isNameExpr()) { + resolved = scope.asNameExpr().resolve(); + } else { + // If not a NameExpr or FieldAccessExpr, then we can't gain any more information, since + // the type of the scope is unsolved. + return; + } + + List variables; + + Node toAst = JavaParserUtil.findAttachedNode(resolved, fqnsToCompilationUnits); + + if (toAst instanceof VariableDeclarationExpr initializer) { + variables = initializer.getVariables(); + } else if (toAst instanceof FieldDeclaration fieldDecl) { + variables = fieldDecl.getVariables(); + } else if (toAst instanceof VariableDeclarator varDecl) { + variables = List.of(varDecl); + } else { + variables = List.of(); + } + + for (VariableDeclarator varDecl : variables) { + if (varDecl.getInitializer().isPresent() + && JavaParserUtil.isExprTypeResolvable(varDecl.getInitializer().get())) { + potentialTypes.add(varDecl.getInitializer().get().calculateResolvedType()); + } + } + } catch (UnsolvedSymbolException ex) { + // Initializer could not be resolved, but the field could still be set somewhere + } + + // Now, find all places where the NameExpr/FieldAccessExpr is set to another type + TypeDeclaration typeDecl = JavaParserUtil.getEnclosingClassLike(methodCall); + + potentialTypes.addAll( + typeDecl.findAll(AssignExpr.class).stream() + .filter( + assign -> + assign.getOperator() == AssignExpr.Operator.ASSIGN + && assign.getTarget().toString().equals(scope.toString()) + && JavaParserUtil.isExprTypeResolvable(assign.getValue())) + .map(assign -> assign.calculateResolvedType()) + .toList()); + + String methodSignature = potentialFQNs.iterator().next(); + methodSignature = methodSignature.substring(potentialFQNs.iterator().next().indexOf('#') + 1); + + List resolvedReturnTypes = new ArrayList<>(); + List unsolvedReturnTypes = new ArrayList<>(); + + for (ResolvedType type : potentialTypes) { + // Check to see if any of these contain the same method signature; if so, we can + // update the return type of the current generated one to match it + + // Must be a reference type: if it were not, the method would be solvable, which we + // checked already + ResolvedReferenceType refType = type.asReferenceType(); + + // The type must also be a user-defined class, not a built-in Java class. This means we + // cannot get the ResolvedMethodDeclarations from each type declaration since parameter + // types could be unsolved + if (refType.getTypeDeclaration().isPresent()) { + for (ResolvedMethodDeclaration methodDecl : + refType.getTypeDeclaration().get().getDeclaredMethods()) { + MethodDeclaration methodDeclAst = + (MethodDeclaration) + JavaParserUtil.findAttachedNode(methodDecl, fqnsToCompilationUnits); + + String signature = + methodDeclAst.getNameAsString() + + "(" + + String.join( + ", ", + methodDeclAst.getParameters().stream() + .map( + param -> + JavaParserUtil.getSimpleNameFromQualifiedName( + param.getTypeAsString())) + .toList()) + + ")"; + + if (signature.equals(methodSignature)) { + try { + resolvedReturnTypes.add(methodDecl.getReturnType()); + } catch (UnsolvedSymbolException ex) { + MemberType returnType = + getMemberTypeFromFQNs( + fullyQualifiedNameGenerator.getFQNsFromType(methodDeclAst.getType()), + false); + + if (returnType == null) { + throw new RuntimeException( + "Unsolved return type when all types should be generated: " + + methodDeclAst.getType()); + } + unsolvedReturnTypes.add((UnsolvedMemberType) returnType); + } + } + } + } + } + + // Note that resolvedReturnTypes and solvedReturnTypes do not contain all the possible return + // types. Typically, it'll only contain one type in total (i.e., the return type of the + // method) directly corresponding with the current method call. Here, we'll add the inferred + // return type of the unsolved super type's method, or the previously calculated lub, and + // recalculate the new lub. + for (MemberType returnType : alt.getReturnTypes()) { + if (resolvedReturnTypes.isEmpty() && returnType instanceof UnsolvedMemberType unsolved) { + if (unsolvedReturnTypes.isEmpty()) { + // nothing to do + continue; + } + + // In this case, set lub to the first encounter + UnsolvedMemberType lub = unsolvedReturnTypes.get(0); + + for (int i = 1; i < unsolvedReturnTypes.size(); i++) { + unsolvedReturnTypes.get(i).getUnsolvedType().addSuperType(Set.of(lub)); + } + + unsolved.getUnsolvedType().addSuperType(Set.of(lub)); + + alt.replaceReturnType(returnType, lub); + } else { + List solvedReturnTypeAsList = + returnType instanceof SolvedMemberType solved ? List.of(solved) : List.of(); + + if (resolvedReturnTypes.isEmpty()) { + // The current return type is equal to the least upper bound, so we don't need to do + // anything + continue; + } + + ResolvedReferenceTypeDeclaration lub = + JavaParserUtil.getLeastUpperBound(resolvedReturnTypes, solvedReturnTypeAsList); + + if (lub == null) { + boolean found = false; + // If null, then a type is a primitive/void + for (ResolvedType type : resolvedReturnTypes) { + alt.replaceReturnType(returnType, new SolvedMemberType(type.describe())); + found = true; + break; + } + + if (!found) { + for (SolvedMemberType solved : solvedReturnTypeAsList) { + String type = solved.getFullyQualifiedNames().iterator().next(); + if (JavaLangUtils.isPrimitive(type) || type.equals("void")) { + alt.replaceReturnType(returnType, solved); + break; + } + } + } + } else { + // Set type parameters to make sure we implement/extend the generic version, not the raw + // type + SolvedMemberType asSolvedMemberType = + new SolvedMemberType( + lub.getQualifiedName(), + Collections.nCopies( + lub.getTypeParameters().size(), WildcardMemberType.UNBOUNDED)); + + if (unsolvedReturnTypes.isEmpty()) { + alt.replaceReturnType(returnType, asSolvedMemberType); + } + } + } + } + } + } + + /** + * Given the possible declaring type fully qualified names and potential method call FQNs, check + * to see if this is defined in java.lang.Throwable. + * + * @param potentialScopeFQNs The potential declaring type fully qualified names + * @param potentialFQNs The potential method call fully qualified names + * @return True if this method call is a java.lang.Throwable method + */ + private boolean isMethodABuiltInThrowableMethod( + Collection> potentialScopeFQNs, Set potentialFQNs) { + for (Set set : potentialScopeFQNs) { + UnsolvedClassOrInterfaceAlternates generatedType = + (UnsolvedClassOrInterfaceAlternates) findExistingAndUpdateFQNs(set); + if (generatedType != null + && (generatedType.doesExtend(SolvedMemberType.JAVA_LANG_EXCEPTION) + || generatedType.doesExtend(SolvedMemberType.JAVA_LANG_ERROR))) { + if (potentialFQNs.stream() + .map(fqn -> fqn.substring(fqn.indexOf('#') + 1)) + .anyMatch(fqn -> JavaLangUtils.getJavaLangThrowableMethods().containsKey(fqn))) { + return true; + } + } + } + + return false; + } + + /** + * Handles the relationship between the LHS and RHS types by making the type of the LHS a + * supertype of the type of the RHS, if the type of the RHS is unsolved. + * + * @param lhsTypes The type(s) of the LHS + * @param rhsTypes The type(s) of the RHS + * @param getResolvedTypeOfLHS A supplier for the resolved type of the LHS. Typically a call to + * resolve() or calculateResolvedType(). + */ + private void handleLHSAndRHSRelationship( + Set lhsTypes, + Set rhsTypes, + Supplier getResolvedTypeOfLHS) { + + @Nullable ResolvedType resolved; + try { + resolved = getResolvedTypeOfLHS.get(); + } catch (UnsolvedSymbolException ex) { + resolved = null; + } + + // Make sure all erasures of the RHS types are handled with the LHS types + for (MemberType rhsType : rhsTypes) { + // If RHS is solvable, do not continue + if (!(rhsType instanceof UnsolvedMemberType unsolved)) { + continue; + } + + if (resolved != null) { + if (resolved.isReferenceType() + && resolved.asReferenceType().getTypeDeclaration().isPresent()) { + ResolvedReferenceTypeDeclaration decl = + resolved.asReferenceType().getTypeDeclaration().get(); + + // If LHS is solvable, there is only one + if (decl.isClass()) { + unsolved.getUnsolvedType().forceSuperClass(lhsTypes.iterator().next()); + } else if (decl.isInterface()) { + unsolved.getUnsolvedType().forceSuperInterface(lhsTypes.iterator().next()); + } else { + throw new RuntimeException("Invalid LHS type: " + resolved.describe()); + } + } + } else { + unsolved.getUnsolvedType().addSuperType(lhsTypes); + } + } + + // Now, make sure the type parameters also have the same relationship, if the left hand side is + // a bounded wildcard + if (resolved != null) { + if (!resolved.isReferenceType()) { + return; + } + + List> typeParameters = + resolved.asReferenceType().getTypeParametersMap(); + for (int i = 0; i < typeParameters.size(); i++) { + ResolvedType typeParam = typeParameters.get(i).b; + + if (typeParam.isWildcard() && typeParam.asWildcard().isBounded()) { + ResolvedType bound = typeParam.asWildcard().getBoundedType(); + boolean isUpperBound = typeParam.asWildcard().isUpperBounded(); + + String erased = JavaParserUtil.erase(resolved.describe()); + + Set rhsTypeParameters = new LinkedHashSet<>(); + for (MemberType rhsType : rhsTypes) { + // There are many possibilities for this: for example, if rhsType is a raw type, + // if rhsType is a non-generic type that extends a generic type, etc. + if (rhsType.getTypeArguments().size() <= i) { + continue; + } + + MemberType typeArg = rhsType.getTypeArguments().get(i); + + if (typeArg instanceof WildcardMemberType rhsWildcard) { + MemberType memberTypeBound = rhsWildcard.getBound(); + + if (memberTypeBound != null) { + typeArg = memberTypeBound; + } + } + + if (!rhsType.getFullyQualifiedNames().stream().anyMatch(erased::contains)) { + continue; + } + + rhsTypeParameters.add(typeArg); + } + + // ? extends with ? extends; there is no ? extends with ? super + if (isUpperBound) { + MemberType memberTypeBound = lhsTypes.iterator().next().getTypeArguments().get(i); + + memberTypeBound = ((WildcardMemberType) memberTypeBound).getBound(); + + if (memberTypeBound == null) { + throw new RuntimeException( + "Null member type wildcard bound when resolved wildcard bound is not null"); + } + + handleLHSAndRHSRelationship(Set.of(memberTypeBound), rhsTypeParameters, () -> bound); + } else { + // If the LHS were unsolved, we would make it extend every single class in the RHS; but + // since the LHS is solved, we can't do this + + // If an issue arises in the future, we could find the unsolvable super classes of this + // resolvable type bound and then apply these bounds there + } + } + } + + return; + } + + for (MemberType lhsType : lhsTypes) { + for (int i = 0; i < lhsType.getTypeArguments().size(); i++) { + MemberType typeParam = lhsType.getTypeArguments().get(i); + + if (!(typeParam instanceof WildcardMemberType wildcard) + || wildcard.equals(WildcardMemberType.UNBOUNDED)) { + continue; + } + + MemberType bound = wildcard.getBound(); + + if (bound == null) { + continue; + } + boolean isUpperBound = wildcard.isUpperBounded(); + + Set erased = + lhsType.getFullyQualifiedNames().stream() + .map(JavaParserUtil::erase) + .collect(Collectors.toSet()); + + Set rhsTypeParameters = new LinkedHashSet<>(); + for (MemberType rhsType : rhsTypes) { + // There are many possibilities for this: for example, if rhsType is a raw type, + // if rhsType is a non-generic type that extends a generic type, etc. + if (rhsType.getTypeArguments().size() <= i) { + continue; + } + + MemberType typeArg = rhsType.getTypeArguments().get(i); + + if (typeArg instanceof WildcardMemberType rhsWildcard) { + MemberType memberTypeBound = rhsWildcard.getBound(); + + if (memberTypeBound != null) { + typeArg = memberTypeBound; + } + } + + if (!rhsType.getFullyQualifiedNames().stream().anyMatch(erased::contains)) { + continue; + } + + rhsTypeParameters.add(typeArg); + } + + // ? extends with ? extends; there is no ? extends with ? super + if (isUpperBound) { + handleLHSAndRHSRelationship( + Set.of(bound), + rhsTypeParameters, + () -> { + throw new UnsolvedSymbolException(""); + }); + } else { + handleLHSAndRHSRelationship( + rhsTypeParameters, + rhsTypes, + () -> { + throw new UnsolvedSymbolException(""); + }); + } + } + } + } + + /** + * Returns the FQNs for an unsolvable constructor call. + * + * @param node The node representing the constructor call; either an ObjectCreationExpr or + * ExplicitConstructorInvocationStmt + * @return A set of FQNs representing the constructor + */ + private Set getFQNsForUnsolvableConstructor(Node node) { + UnsolvedClassOrInterfaceAlternates scope; + String constructorName; + List arguments; + + if (node instanceof ObjectCreationExpr constructor) { + scope = + (UnsolvedClassOrInterfaceAlternates) + findExistingAndUpdateFQNs( + fullyQualifiedNameGenerator.getFQNsFromType(constructor.getType())); + + constructorName = constructor.getTypeAsString(); + arguments = constructor.getArguments(); + } else if (node instanceof ExplicitConstructorInvocationStmt constructor) { + // If it's unresolvable, it's a constructor in the unsolved parent class + if (!constructor.isThis()) { + // There can only be one extends in a class + ClassOrInterfaceType superClass = JavaParserUtil.getSuperClass(node); + + scope = + (UnsolvedClassOrInterfaceAlternates) + findExistingAndUpdateFQNs(fullyQualifiedNameGenerator.getFQNsFromType(superClass)); + + constructorName = superClass.getNameAsString(); + arguments = constructor.getArguments(); + } else { + // We should never reach this case unless the user inputted a bad program (i.e. + // this(...) constructor call when a definition is not there, or super() without a parent + // class) + throw new RuntimeException("Unexpected explicit constructor invocation statement call."); + } + } else { + throw new RuntimeException( + "Parameter node must be an ObjectCreationExpr or an ExplicitConstructorInvocationStmt: " + + node.getClass()); + } + + if (scope == null) { + throw new RuntimeException("Scope not created when it should've been"); + } + + constructorName = + JavaParserUtil.getSimpleNameFromQualifiedName(JavaParserUtil.erase(constructorName)); + + List> simpleNames = new ArrayList<>(); + + for (Expression argument : arguments) { + Set simpleNamesForArgument = new LinkedHashSet<>(); + for (FullyQualifiedNameSet fqns : + fullyQualifiedNameGenerator.getFQNsForExpressionType(argument)) { + String first = fqns.erasedFqns().iterator().next(); + simpleNamesForArgument.add(JavaParserUtil.getSimpleNameFromQualifiedName(first)); + } + simpleNames.add(simpleNamesForArgument); + } + + Set potentialFQNs = new LinkedHashSet<>(); + + for (List simpleNameList : JavaParserUtil.generateAllCombinations(simpleNames)) { + for (String potentialScopeFQN : scope.getFullyQualifiedNames()) { + potentialFQNs.add( + potentialScopeFQN + + "#" + + constructorName + + "(" + + String.join(", ", simpleNameList) + + ")"); + } + } + + return potentialFQNs; + } + + /** + * Once {@link #addInformation(Node)} is done, call this method to make sure all generated symbols + * are consistent with their super type relationships. + */ + public void generateAllAlternatesBasedOnSuperTypeRelationships() { + // This method is called after all unsolved symbols are generated and all information is added + // to ensure that all symbols are consistent with their super type relationships. + for (UnsolvedSymbolAlternates symbol : Set.copyOf(generatedSymbols.values())) { + if (symbol instanceof UnsolvedClassOrInterfaceAlternates type) { + type.createAlternatesBasedOnSuperTypeRelationships(); + } + } + } + + /** + * Call this the first time a type is set to extend a Throwable (Exception, Error, itself, etc.). + * This removes all methods that may have been generated for the type but also exists in the + * Throwable class. This is an expensive call. + * + * @param type The type that extends Throwable + * @return Symbols that need to be removed + */ + private List> handleExtendThrowable( + UnsolvedClassOrInterfaceAlternates type) { + // Remove all methods that are already defined in Throwable + // This is because the type is now a Throwable, so it cannot have its own methods + // that are already defined in Throwable. + + // Method to remove to the proper signature + Map methodsToRemove = new HashMap<>(); + Map methods = JavaLangUtils.getJavaLangThrowableMethods(); + + for (UnsolvedSymbolAlternates symbol : generatedSymbols.values()) { + if (symbol instanceof UnsolvedMethodAlternates method) { + if (method.getAlternateDeclaringTypes().contains(type)) { + String fqn = + method.getFullyQualifiedNames().stream() + .map(f -> f.substring(f.indexOf('#') + 1)) + .filter(f -> methods.containsKey(f)) + .findFirst() + .orElse(null); + if (fqn != null) { + methodsToRemove.put(method, fqn); + } + } + } + } + + Map typeCorrect = new HashMap<>(); + for (Entry entry : methodsToRemove.entrySet()) { + UnsolvedMethodAlternates method = entry.getKey(); + String methodSignature = entry.getValue(); + String correctReturnType = methods.get(methodSignature); + + if (correctReturnType == null) { + throw new RuntimeException("Unknown method signature: " + methodSignature); + } + + // Remove all instances of the synthetic return type + for (MemberType returnType : method.getReturnTypes()) { + if (returnType instanceof UnsolvedMemberType unsolvedReturn) { + UnsolvedClassOrInterfaceAlternates unsolvedType = unsolvedReturn.getUnsolvedType(); + typeCorrect.put(unsolvedType, new SolvedMemberType(correctReturnType)); + } + } + } + + Set keysToRemove = new HashSet<>(); + Set methodsWithChangedSignatures = new HashSet<>(); + for (UnsolvedSymbolAlternates symbol : generatedSymbols.values()) { + if (symbol instanceof UnsolvedMethodAlternates method) { + for (MemberType returnType : method.getReturnTypes()) { + if (returnType instanceof UnsolvedMemberType unsolvedReturn) { + UnsolvedClassOrInterfaceAlternates unsolvedType = unsolvedReturn.getUnsolvedType(); + SolvedMemberType correct = typeCorrect.get(unsolvedType); + if (correct != null) { + method.replaceReturnType(unsolvedReturn, correct); + } + } + } + + Set oldSignatures = method.getFullyQualifiedNames(); + boolean signatureChanged = false; + for (UnsolvedMethod alternate : method.getAlternates()) { + for (MemberType paramType : alternate.getParameterList()) { + if (paramType instanceof UnsolvedMemberType unsolvedParam) { + UnsolvedClassOrInterfaceAlternates unsolvedType = unsolvedParam.getUnsolvedType(); + SolvedMemberType correct = typeCorrect.get(unsolvedType); + if (correct != null) { + alternate.replaceParameterType(unsolvedParam, correct); + signatureChanged = true; + } + } + } + } + + if (signatureChanged) { + keysToRemove.addAll(oldSignatures); + methodsWithChangedSignatures.add(method); + } + + method.removeDuplicateAlternates(); + } else if (symbol instanceof UnsolvedFieldAlternates field) { + for (MemberType fieldType : field.getTypes()) { + if (fieldType instanceof UnsolvedMemberType unsolvedType) { + UnsolvedClassOrInterfaceAlternates unsolvedClass = unsolvedType.getUnsolvedType(); + SolvedMemberType correct = typeCorrect.get(unsolvedClass); + if (correct != null) { + field.replaceFieldType(unsolvedType, correct); + } + } + } + + field.removeDuplicateAlternates(); + } + } + + for (String signatureToRemove : keysToRemove) { + generatedSymbols.remove(signatureToRemove); + } + + for (UnsolvedMethodAlternates method : methodsWithChangedSignatures) { + addNewSymbolToGeneratedSymbolsMap(method); + } + + List> toRemove = new ArrayList<>(methodsToRemove.keySet()); + toRemove.addAll(typeCorrect.keySet()); + + for (UnsolvedSymbolAlternates symbol : toRemove) { + removeSymbolFromGeneratedSymbolsMap(symbol); + } + + return toRemove; + } + + /** + * Returns whether a node needs to undergo post-processing or not; i.e., if {@link + * #addInformation(Node)} needs to be called on it. This is used in the initial worklist when some + * unsolved symbols may not be generated yet to defer additional information processing to a time + * when all unsolved symbols are generated. + * + * @param node The node to query about + * @return Whether {@link #addInformation(Node)} accepts this node + */ + public boolean needToPostProcess(Node node) { + return node instanceof ClassOrInterfaceDeclaration + || node instanceof EnumDeclaration + || node instanceof MethodDeclaration + || node instanceof TryStmt + || node instanceof ThrowStmt + || node instanceof InstanceOfExpr + || node instanceof MethodCallExpr + || node instanceof TypeParameter + || node instanceof AssignExpr + || node instanceof ReturnStmt + || node instanceof VariableDeclarator + || node instanceof LambdaExpr + || node instanceof ObjectCreationExpr + || node instanceof ExplicitConstructorInvocationStmt + || node instanceof EnumConstantDeclaration; + } + + /** + * Converts a set of FullyQualifiedNameSet to a set of MemberType. Throws if any + * FullyQualifiedNameSet doesn't correspond with a generated MemberType. + * + * @param fqnSets The set of FullyQualifiedNameSet to convert. + * @return A set of MemberType corresponding to the input FQNSets. + */ + private Set getMemberTypesAndExpectNonNullFromFQNSets( + Set fqnSets) { + Set memberTypes = new LinkedHashSet<>(); + + for (FullyQualifiedNameSet fqnSet : fqnSets) { + MemberType genType = getMemberTypeFromFQNs(fqnSet, false); + + if (genType == null) { + throw new RuntimeException("Unresolved type when we expect a generated type: " + fqnSet); + } + + memberTypes.add(genType); + } + + return memberTypes; + } + + /** + * Same as {@link #findExistingAndUpdateFQNs(Set)} but creates and returns a new type if not + * found. This only works for type FQNs. + * + * @param fqns The set of fqns + * @return The existing or created definition + */ + private UnsolvedClassOrInterfaceAlternates findExistingAndUpdateFQNsOrCreateNewType( + Set fqns) { + UnsolvedSymbolAlternates existing = findExistingAndUpdateFQNs(fqns); + + if (existing == null) { + List created = + UnsolvedClassOrInterfaceAlternates.create(fqns, generatedSymbols); + + for (UnsolvedClassOrInterfaceAlternates c : created) { + addNewSymbolToGeneratedSymbolsMap(c); + } + return created.get(0); + } + + return (UnsolvedClassOrInterfaceAlternates) existing; + } + + /** + * Short-hand call for {@link #findExistingAndUpdateFQNs(FullyQualifiedNameSet)} that takes a + * {@link FullyQualifiedNameSet} as input. + * + * @param potentialFQNs The set of potential FQNs + * @return The existing symbol, or null if one does not exist yet. + * @see #findExistingAndUpdateFQNs(Set) + */ + private @Nullable UnsolvedSymbolAlternates findExistingAndUpdateFQNs( + FullyQualifiedNameSet potentialFQNs) { + return findExistingAndUpdateFQNs(potentialFQNs.erasedFqns()); + } + + /** + * Finds the existing unsolved symbol based on a set of potential FQNs. If none is found, this + * method returns null. The generatedSymbols map is also modified if the intersection of + * potentialFQNs and the existing set results in a smaller set of potential FQNs. + * + * @param potentialFQNs The set of potential fully-qualified names (type arguments erased) in the + * current context. + * @return The existing symbol, or null if one does not exist yet. + */ + private @Nullable UnsolvedSymbolAlternates findExistingAndUpdateFQNs( + Set potentialFQNs) { + // There is likely only an overlap of FQNs if the two types refer to the same type, + // but one of these instances may know more information than the other. If it already + // exists in the generatedSymbols set, we'll keep the most specific set of potential + // FQNs. + + // For example, if we have in the map an UnsolvedSymbolAlternates with ambiguous mappings + // of class A: {org.example.A, org.example.ParentClass.A} --> defn, but then we encounter + // a file with an explicit import org.example.A;, then we know for sure that this type + // refers to org.example.A, so we'll remove it from the alternates set. + + UnsolvedSymbolAlternates alreadyGenerated = null; + for (String potentialFQN : potentialFQNs) { + alreadyGenerated = generatedSymbols.get(potentialFQN); + + if (alreadyGenerated != null) { + break; + } + } + + if (alreadyGenerated != null) { + UnsolvedClassOrInterfaceAlternates type = null; + + if (alreadyGenerated instanceof UnsolvedClassOrInterfaceAlternates) { + type = (UnsolvedClassOrInterfaceAlternates) alreadyGenerated; + } else { + for (String potentialFQN : potentialFQNs) { + UnsolvedSymbolAlternates potentialType = + generatedSymbols.get(potentialFQN.substring(0, potentialFQN.indexOf('#'))); + + if (potentialType instanceof UnsolvedClassOrInterfaceAlternates) { + type = (UnsolvedClassOrInterfaceAlternates) potentialType; + break; + } + } + } + + if (type == null) { + throw new RuntimeException( + "Cannot have generated fields/methods before its type is generated."); + } + + Set alreadyGeneratedFQNs = alreadyGenerated.getFullyQualifiedNames(); + + if (!potentialFQNs.equals(alreadyGeneratedFQNs)) { + for (String oldFQN : alreadyGeneratedFQNs) { + generatedSymbols.remove(oldFQN); + } + + Set typeFQNs = potentialFQNs; + + if (!(alreadyGenerated instanceof UnsolvedClassOrInterfaceAlternates)) { + typeFQNs = + potentialFQNs.stream() + .map(fqn -> fqn.substring(0, fqn.indexOf('#'))) + .collect(Collectors.toSet()); + } + + // TODO before you push this commit: for methods, only return alreadyGenerated if + // the parameter types match too. If the input is a subset of all the fqns, then + // this is likely another method that we have to generate (this can happen when + // there are ambiguous method references passed in as an argument) + + type.updateFullyQualifiedNames(typeFQNs); + + for (String newFQN : alreadyGenerated.getFullyQualifiedNames()) { + generatedSymbols.put(newFQN, alreadyGenerated); + } + } + } + + return alreadyGenerated; + } + + /** + * Helper method to add a new symbol to {@link #generatedSymbols}. + * + * @param newSymbol The new symbol to add + */ + private void addNewSymbolToGeneratedSymbolsMap(UnsolvedSymbolAlternates newSymbol) { + for (String potentialFQN : newSymbol.getFullyQualifiedNames()) { + if (generatedSymbols.containsKey(potentialFQN)) { + continue; + } + generatedSymbols.put(potentialFQN, newSymbol); + } + } + + /** + * Helper method to remove a symbol from {@link #generatedSymbols}. + * + * @param symbol The symbol to remove + */ + private void removeSymbolFromGeneratedSymbolsMap(UnsolvedSymbolAlternates symbol) { + for (String potentialFQN : symbol.getFullyQualifiedNames()) { + generatedSymbols.remove(potentialFQN); + } + } + + /** + * Gets the {@code MemberType} from a set of FQNs. If one of the FQNs represents a primitive or + * built-in java class, then it returns that type. If not, then this method will find an existing + * generated type, or create it, and return it. + * + * @param fqns The set of fully-qualified names + * @return The member type + */ + private MemberType getOrCreateMemberTypeFromFQNs(FullyQualifiedNameSet fqns) { + MemberType memberType = getMemberTypeFromFQNs(fqns, true); + + if (memberType == null) { + throw new RuntimeException("This error is impossible."); + } + + return memberType; + } + + /** + * Returns true if any fqn in the set represents a type included in the input or in the JDK. + * + * @param fqns The set of fully-qualified names to check + * @return True if the set overlaps with known types, false otherwise + */ + private boolean doesOverlapWithKnownType(Set fqns) { + for (String fqn : fqns) { + if (fqnsToCompilationUnits.containsKey(fqn) + || JavaLangUtils.inJdkPackage(JavaParserUtil.removeArrayBrackets(fqn)) + || JavaLangUtils.isJavaLangOrPrimitiveName( + JavaParserUtil.getSimpleNameFromQualifiedName( + JavaParserUtil.removeArrayBrackets(fqn)))) { + return true; + } + } + return false; + } + + /** + * Gets the {@code MemberType} from a set of FQNs. If one of the FQNs represents a primitive or + * built-in java class, then it returns that type. If not, then this method will find an existing + * generated type (or create it, depending on {@code createNew}), and return it. If there are type + * arguments, please fully qualify them before passing into this method. + * + * @param fqns The set of fully-qualified names + * @return The member type + */ + private @Nullable MemberType getMemberTypeFromFQNs( + FullyQualifiedNameSet fqns, boolean createNew) { + String wildcard = fqns.wildcard(); + if (wildcard != null) { + if (wildcard.equals(FullyQualifiedNameSet.UNBOUNDED_WILDCARD.wildcard())) { + return WildcardMemberType.UNBOUNDED; + } + + if (wildcard.equals("? extends")) { + return new WildcardMemberType( + getMemberTypeFromFQNs( + new FullyQualifiedNameSet(fqns.erasedFqns(), fqns.typeArguments()), createNew), + true); + } else if (wildcard.equals("? super")) { + return new WildcardMemberType( + getMemberTypeFromFQNs( + new FullyQualifiedNameSet(fqns.erasedFqns(), fqns.typeArguments()), createNew), + false); + } + + throw new RuntimeException("Unexpected wildcard: " + wildcard); + } + + List typeArguments = new ArrayList<>(); + + for (FullyQualifiedNameSet typeArg : fqns.typeArguments()) { + MemberType memberType = getMemberTypeFromFQNs(typeArg, createNew); + + if (memberType == null) { + throw new RuntimeException("Type arguments must be generated."); + } + + typeArguments.add(memberType); + } + + for (String fqn : fqns.erasedFqns()) { + if (fqnsToCompilationUnits.containsKey(JavaParserUtil.removeArrayBrackets(fqn))) { + return new SolvedMemberType(fqn, typeArguments); + } + + MemberType type = getMemberTypeIfPrimitiveOrJavaLang(fqn, typeArguments); + + if (type != null) { + return type; + } + } + + // If a set has one element with no dots, it's likely a type variable + if (fqns.erasedFqns().size() == 1 && !fqns.erasedFqns().iterator().next().contains(".")) { + return new SolvedMemberType(fqns.erasedFqns().iterator().next()); + } + + UnsolvedClassOrInterfaceAlternates unsolved; + + Set fqnsWithoutArray = new LinkedHashSet<>(); + + for (String fqn : fqns.erasedFqns()) { + fqnsWithoutArray.add(JavaParserUtil.removeArrayBrackets(fqn)); + } + + if (createNew) { + unsolved = findExistingAndUpdateFQNsOrCreateNewType(fqnsWithoutArray); + } else { + unsolved = (UnsolvedClassOrInterfaceAlternates) findExistingAndUpdateFQNs(fqnsWithoutArray); + } + + if (unsolved == null) { + return null; + } else { + return new UnsolvedMemberType( + unsolved, + JavaParserUtil.countNumberOfArrayBrackets(fqns.erasedFqns().iterator().next()), + typeArguments); + } + } + + /** + * If {@code name} (either a simple name or fully qualified) is primitive, java.lang, or in + * another java package, then return the MemberType holding it. Else, return null. + * + * @param name The name of the type, either simple or fully qualified. + * @param typeArguments The type arguments of the type, if any. + */ + private @Nullable MemberType getMemberTypeIfPrimitiveOrJavaLang( + String name, List typeArguments) { + if (JavaLangUtils.inJdkPackage(JavaParserUtil.removeArrayBrackets(name)) + || JavaLangUtils.isJavaLangOrPrimitiveName( + JavaParserUtil.getSimpleNameFromQualifiedName(JavaParserUtil.removeArrayBrackets(name))) + || name.equals("void")) { + return new SolvedMemberType(name, typeArguments); + } + return null; + } +} diff --git a/src/main/java/org/checkerframework/specimin/unsolved/WildcardMemberType.java b/src/main/java/org/checkerframework/specimin/unsolved/WildcardMemberType.java new file mode 100644 index 000000000..ed437abfd --- /dev/null +++ b/src/main/java/org/checkerframework/specimin/unsolved/WildcardMemberType.java @@ -0,0 +1,116 @@ +package org.checkerframework.specimin.unsolved; + +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import org.checkerframework.checker.nullness.qual.Nullable; + +/** Represents a wildcard type (i.e., ?, ? extends T, or ? super T). */ +public class WildcardMemberType extends MemberType { + /** Represents the type for an unbounded wildcard: ? */ + public static final WildcardMemberType UNBOUNDED = new WildcardMemberType(null, false); + + /** The bound of the wildcard, or null if unbounded. */ + private final @Nullable MemberType bound; + + /** + * If bound is not null, this indicates whether the wildcard is an upper bound (? extends) or a + * lower bound (? super). + */ + private final boolean isUpperBound; + + /** + * Creates a new WildcardMemberType with an optional bound. If the bound is null, it represents an + * unbounded wildcard (i.e., "?"). If the bound is not null, use isUpperBound to determine whether + * this is an upper or lower bound wildcard (i.e., "? extends T" or "? super T"). + * + * @param bound The bound of the wildcard, or null for an unbounded wildcard. + * @param isUpperBound True if this is an upper bound wildcard, false if it is a lower bound. If + * bound is null, this parameter is ignored. + */ + public WildcardMemberType(@Nullable MemberType bound, boolean isUpperBound) { + super(List.of()); + this.bound = bound; + this.isUpperBound = isUpperBound; + } + + /** + * Gets the bound of this wildcard type. If this is an unbounded wildcard, this will return null. + * + * @return The bound of the wildcard + */ + public @Nullable MemberType getBound() { + return bound; + } + + /** + * Is the bound an upper bound (? extends) or lower bound (? super)? + * + * @return True if this is an upper bound wildcard, false if it is a lower bound. + */ + public boolean isUpperBounded() { + return isUpperBound; + } + + @Override + public Set getFullyQualifiedNames() { + Set fqnSet = new LinkedHashSet<>(); + + if (bound == null) { + return Set.of("?"); + } + + String boundString = getBoundString(); + for (String fqn : bound.getFullyQualifiedNames()) { + fqnSet.add(boundString + fqn); + } + return fqnSet; + } + + /** + * Gets the string representation of the wildcard's bound, including the wildcard symbol and the + * appropriate keyword ("extends" or "super") if the bound is not null. + * + * @return The string representation of the wildcard's bound. + */ + private String getBoundString() { + if (bound == null) { + return "?"; + } + return isUpperBound ? "? extends " : "? super "; + } + + @Override + public String toString() { + if (bound == null) { + return "?"; + } + + return getBoundString() + bound.toString(); + } + + @Override + public boolean equals(@Nullable Object other) { + if (!(other instanceof WildcardMemberType otherAsWildcard)) { + return false; + } + + return Objects.equals(otherAsWildcard.bound, this.bound) + && Objects.equals(otherAsWildcard.getBoundString(), this.getBoundString()); + } + + @Override + public int hashCode() { + return Objects.hash(bound, getBoundString()); + } + + @Override + public MemberType copyWithNewTypeArgs(List newTypeArgs) { + if (newTypeArgs.isEmpty()) { + return new WildcardMemberType(bound, isUpperBound); + } else { + throw new RuntimeException("WildcardMemberType cannot have type arguments"); + } + } +} diff --git a/src/test/java/org/checkerframework/specimin/AbstractSuperWithConcreteImplTest.java b/src/test/java/org/checkerframework/specimin/AbstractSuperWithConcreteImplTest.java new file mode 100644 index 000000000..993ebdad3 --- /dev/null +++ b/src/test/java/org/checkerframework/specimin/AbstractSuperWithConcreteImplTest.java @@ -0,0 +1,18 @@ +package org.checkerframework.specimin; + +import java.io.IOException; +import org.junit.Test; + +/** + * This test checks to see if methods implemented in a concrete class that implements an + * interface/abstract class are preserved when only abstract super methods are called. + */ +public class AbstractSuperWithConcreteImplTest { + @Test + public void runTest() throws IOException { + SpeciminTestExecutor.runTestWithoutJarPaths( + "abstractsuperwithconcreteimpl", + new String[] {"com/example/Simple.java"}, + new String[] {"com.example.Simple#foo()"}); + } +} diff --git a/src/test/java/org/checkerframework/specimin/FinalFieldWithNonTargetConstructorTest.java b/src/test/java/org/checkerframework/specimin/FinalFieldWithNonTargetConstructorTest.java new file mode 100644 index 000000000..78a8845fd --- /dev/null +++ b/src/test/java/org/checkerframework/specimin/FinalFieldWithNonTargetConstructorTest.java @@ -0,0 +1,18 @@ +package org.checkerframework.specimin; + +import java.io.IOException; +import org.junit.Test; + +/** + * This test checks that an initializer is added to a final field if it was originally set in a + * non-target constructor. + */ +public class FinalFieldWithNonTargetConstructorTest { + @Test + public void runTest() throws IOException { + SpeciminTestExecutor.runTestWithoutJarPaths( + "finalfieldwithnontargetconstructor", + new String[] {"com/example/Foo.java"}, + new String[] {"com.example.Foo#bar()"}); + } +} diff --git a/src/test/java/org/checkerframework/specimin/ImportAnnoTest.java b/src/test/java/org/checkerframework/specimin/ImportAnnoTest.java index d072367b7..897fc1cbd 100644 --- a/src/test/java/org/checkerframework/specimin/ImportAnnoTest.java +++ b/src/test/java/org/checkerframework/specimin/ImportAnnoTest.java @@ -15,6 +15,7 @@ public void runTest() throws IOException { new String[] {"com/example/Simple.java"}, new String[] {"com.example.Simple#bar()"}, "cf", - new String[] {"src/test/resources/shared/checker-qual-3.42.0.jar"}); + new String[] {"src/test/resources/shared/checker-qual-3.42.0.jar"}, + "best-effort"); } } diff --git a/src/test/java/org/checkerframework/specimin/JarFileTest.java b/src/test/java/org/checkerframework/specimin/JarFileTest.java index f8ded215a..f8e07c8ed 100644 --- a/src/test/java/org/checkerframework/specimin/JarFileTest.java +++ b/src/test/java/org/checkerframework/specimin/JarFileTest.java @@ -12,6 +12,7 @@ public void runTest() throws IOException { new String[] {"com/example/Simple.java"}, new String[] {"com.example.Simple#test()"}, "cf", - new String[] {"src/test/resources/jarfile/input/Book.jar"}); + new String[] {"src/test/resources/jarfile/input/Book.jar"}, + "best-effort"); } } diff --git a/src/test/java/org/checkerframework/specimin/MustImplementMethodsComplexTest.java b/src/test/java/org/checkerframework/specimin/MustImplementMethodsComplexTest.java new file mode 100644 index 000000000..d494f35ad --- /dev/null +++ b/src/test/java/org/checkerframework/specimin/MustImplementMethodsComplexTest.java @@ -0,0 +1,18 @@ +package org.checkerframework.specimin; + +import java.io.IOException; +import org.junit.Test; + +/** + * This test checks if Specimin correctly preserves methods that are part of a JDK interface with an + * indirect type parameter map (i.e. String --> E to E --> T). + */ +public class MustImplementMethodsComplexTest { + @Test + public void runTest() throws IOException { + SpeciminTestExecutor.runTestWithoutJarPaths( + "mustimplementmethodscomplex", + new String[] {"com/example/Foo.java"}, + new String[] {"com.example.Foo#foo()"}); + } +} diff --git a/src/test/java/org/checkerframework/specimin/ParameterWithAnnotationsTest.java b/src/test/java/org/checkerframework/specimin/ParameterWithAnnotationsTest.java index dcacb3d14..cceb934c1 100644 --- a/src/test/java/org/checkerframework/specimin/ParameterWithAnnotationsTest.java +++ b/src/test/java/org/checkerframework/specimin/ParameterWithAnnotationsTest.java @@ -15,6 +15,7 @@ public void runTest() throws IOException { new String[] {"com/example/Simple.java"}, new String[] {"com.example.Simple#bar(byte[], UnsolvedType)"}, "cf", - new String[] {"src/test/resources/shared/checker-qual-3.42.0.jar"}); + new String[] {"src/test/resources/shared/checker-qual-3.42.0.jar"}, + "best-effort"); } } diff --git a/src/test/java/org/checkerframework/specimin/PreserveAnnotationsTest.java b/src/test/java/org/checkerframework/specimin/PreserveAnnotationsTest.java index ff48ba906..fca5ecab3 100644 --- a/src/test/java/org/checkerframework/specimin/PreserveAnnotationsTest.java +++ b/src/test/java/org/checkerframework/specimin/PreserveAnnotationsTest.java @@ -12,6 +12,7 @@ public void runTest() throws IOException { new String[] {"com/example/Simple.java"}, new String[] {"com.example.Simple#test()"}, "cf", - new String[] {"src/test/resources/shared/checker-qual-3.42.0.jar"}); + new String[] {"src/test/resources/shared/checker-qual-3.42.0.jar"}, + "best-effort"); } } diff --git a/src/test/java/org/checkerframework/specimin/SpeciminTestExecutor.java b/src/test/java/org/checkerframework/specimin/SpeciminTestExecutor.java index 879361a77..dabc723da 100644 --- a/src/test/java/org/checkerframework/specimin/SpeciminTestExecutor.java +++ b/src/test/java/org/checkerframework/specimin/SpeciminTestExecutor.java @@ -36,6 +36,7 @@ private SpeciminTestExecutor() { * class.fully.qualified.Name#fieldName for field. * @param modularityModel the model to use * @param jarPaths the path of jar files for Specimin to solve symbols + * @param ambiguityResolutionPolicy the ambiguity resolution policy to use * @throws IOException if some operation fails */ public static void runTest( @@ -43,7 +44,8 @@ public static void runTest( String[] targetFiles, String[] targetMembers, String modularityModel, - String[] jarPaths) + String[] jarPaths, + String ambiguityResolutionPolicy) throws IOException { // Create output directory Path outputDir = null; @@ -85,9 +87,16 @@ public static void runTest( speciminArgs.add("--jarPath"); speciminArgs.add(jarPath); } + speciminArgs.add("--ambiguityResolutionPolicy"); + speciminArgs.add(ambiguityResolutionPolicy); // Run specimin on target - SpeciminRunner.main(speciminArgs.toArray(new String[0])); + try { + SpeciminRunner.main(speciminArgs.toArray(new String[0])); + } catch (Exception ex) { + ex.printStackTrace(); + throw ex; + } Path expectedDir = Path.of("src/test/resources/" + testName + "/expected/"); assertDirectoriesEqual(expectedDir, outputDir); @@ -141,7 +150,11 @@ private static void assertDirectoriesEqual(Path expectedDir, Path actualDir) thr + actualCu); } } catch (Exception e) { - Assert.fail("Error parsing and comparing files: " + relativePath.toString().replace('\\', '/') + "\n" + e); + Assert.fail( + "Error parsing and comparing files: " + + relativePath.toString().replace('\\', '/') + + "\n" + + e); } } } @@ -160,7 +173,7 @@ private static void assertDirectoriesEqual(Path expectedDir, Path actualDir) thr */ public static void runTestWithoutJarPaths( String testName, String[] targetFiles, String[] targetMembers) throws IOException { - runTest(testName, targetFiles, targetMembers, "cf", new String[] {}); + runTest(testName, targetFiles, targetMembers, "cf", new String[] {}, "best-effort"); } /** @@ -176,6 +189,6 @@ public static void runTestWithoutJarPaths( */ public static void runNullAwayTestWithoutJarPaths( String testName, String[] targetFiles, String[] targetMembers) throws IOException { - runTest(testName, targetFiles, targetMembers, "nullaway", new String[] {}); + runTest(testName, targetFiles, targetMembers, "nullaway", new String[] {}, "best-effort"); } } diff --git a/src/test/java/org/checkerframework/specimin/SuperMethodLub2Test.java b/src/test/java/org/checkerframework/specimin/SuperMethodLub2Test.java new file mode 100644 index 000000000..48df6daff --- /dev/null +++ b/src/test/java/org/checkerframework/specimin/SuperMethodLub2Test.java @@ -0,0 +1,18 @@ +package org.checkerframework.specimin; + +import java.io.IOException; +import org.junit.Test; + +/** + * This test checks to see if Specimin generates the correct least upper bound return type for an + * unsolved super method if there are multiple potential return types. + */ +public class SuperMethodLub2Test { + @Test + public void runTest() throws IOException { + SpeciminTestExecutor.runTestWithoutJarPaths( + "supermethodlub2", + new String[] {"com/example/Simple.java"}, + new String[] {"com.example.Simple#foo()"}); + } +} diff --git a/src/test/java/org/checkerframework/specimin/SuperMethodLubTest.java b/src/test/java/org/checkerframework/specimin/SuperMethodLubTest.java new file mode 100644 index 000000000..009ef3591 --- /dev/null +++ b/src/test/java/org/checkerframework/specimin/SuperMethodLubTest.java @@ -0,0 +1,18 @@ +package org.checkerframework.specimin; + +import java.io.IOException; +import org.junit.Test; + +/** + * This test checks to see if Specimin generates the correct least upper bound return type for an + * unsolved super method if child overrides are known and have different return types. + */ +public class SuperMethodLubTest { + @Test + public void runTest() throws IOException { + SpeciminTestExecutor.runTestWithoutJarPaths( + "supermethodlub", + new String[] {"com/example/Simple.java"}, + new String[] {"com.example.Simple#foo()"}); + } +} diff --git a/src/test/java/org/checkerframework/specimin/SyntheticEnumFromAnnoTest.java b/src/test/java/org/checkerframework/specimin/SyntheticEnumFromAnnoTest.java new file mode 100644 index 000000000..eee4b0d1f --- /dev/null +++ b/src/test/java/org/checkerframework/specimin/SyntheticEnumFromAnnoTest.java @@ -0,0 +1,18 @@ +package org.checkerframework.specimin; + +import java.io.IOException; +import org.junit.Test; + +/** + * This test checks if enums are generated as enums, and not classes, when shown in the context of + * an annotation. + */ +public class SyntheticEnumFromAnnoTest { + @Test + public void runTest() throws IOException { + SpeciminTestExecutor.runTestWithoutJarPaths( + "syntheticenumfromanno", + new String[] {"com/example/Simple.java"}, + new String[] {"com.example.Simple#foo()"}); + } +} diff --git a/src/test/java/org/checkerframework/specimin/TypeArgumentSuperRelationshipTest.java b/src/test/java/org/checkerframework/specimin/TypeArgumentSuperRelationshipTest.java new file mode 100644 index 000000000..748fc5c8b --- /dev/null +++ b/src/test/java/org/checkerframework/specimin/TypeArgumentSuperRelationshipTest.java @@ -0,0 +1,15 @@ +package org.checkerframework.specimin; + +import java.io.IOException; +import org.junit.Test; + +/** This test checks that Specimin correctly handles type argument super relationships. */ +public class TypeArgumentSuperRelationshipTest { + @Test + public void runTest() throws IOException { + SpeciminTestExecutor.runTestWithoutJarPaths( + "typeargumentsuperrelationship", + new String[] {"com/example/Simple.java"}, + new String[] {"com.example.Simple#bar()"}); + } +} diff --git a/src/test/java/org/checkerframework/specimin/UnsolvedLambdaParameterUseTest.java b/src/test/java/org/checkerframework/specimin/UnsolvedLambdaParameterUseTest.java new file mode 100644 index 000000000..1baf7d36d --- /dev/null +++ b/src/test/java/org/checkerframework/specimin/UnsolvedLambdaParameterUseTest.java @@ -0,0 +1,19 @@ +package org.checkerframework.specimin; + +import java.io.IOException; +import org.junit.Test; + +/** + * This test checks that the synthetic interface generated for a lambda parameter has correct type + * arguments based on the usages for a lambda parameter. For example, instead of Consumer, it + * should be Consumer. + */ +public class UnsolvedLambdaParameterUseTest { + @Test + public void runTest() throws IOException { + SpeciminTestExecutor.runTestWithoutJarPaths( + "unsolvedlambdaparameteruse", + new String[] {"com/example/Foo.java"}, + new String[] {"com.example.Foo#bar()"}); + } +} diff --git a/src/test/java/org/checkerframework/specimin/UnsolvedMethodReferenceWithKnownLHSTest.java b/src/test/java/org/checkerframework/specimin/UnsolvedMethodReferenceWithKnownLHSTest.java new file mode 100644 index 000000000..9b916da9c --- /dev/null +++ b/src/test/java/org/checkerframework/specimin/UnsolvedMethodReferenceWithKnownLHSTest.java @@ -0,0 +1,18 @@ +package org.checkerframework.specimin; + +import java.io.IOException; +import org.junit.Test; + +/** + * This test checks that an unsolved method reference with a known left-hand side (LHS) is generated + * with the correct parameter types and voidness. + */ +public class UnsolvedMethodReferenceWithKnownLHSTest { + @Test + public void runTest() throws IOException { + SpeciminTestExecutor.runTestWithoutJarPaths( + "unsolvedmethodreferencewithknownlhs", + new String[] {"com/example/Simple.java"}, + new String[] {"com.example.Simple#foo()"}); + } +} diff --git a/src/test/java/org/checkerframework/specimin/UnsolvableInterfaceTest.java b/src/test/java/org/checkerframework/specimin/UnsolvedSuperConstructor2Test.java similarity index 58% rename from src/test/java/org/checkerframework/specimin/UnsolvableInterfaceTest.java rename to src/test/java/org/checkerframework/specimin/UnsolvedSuperConstructor2Test.java index 875ec2fe7..6c3f0257c 100644 --- a/src/test/java/org/checkerframework/specimin/UnsolvableInterfaceTest.java +++ b/src/test/java/org/checkerframework/specimin/UnsolvedSuperConstructor2Test.java @@ -4,14 +4,14 @@ import org.junit.Test; /** - * This test checks if Specimin can handle unsolvable interfaces that happens due to the an input - * file isolated from its codebase. + * This test checks that if Specimin will work if there is an unsolved super constructor in a target + * method and target constructor. */ -public class UnsolvableInterfaceTest { +public class UnsolvedSuperConstructor2Test { @Test public void runTest() throws IOException { SpeciminTestExecutor.runTestWithoutJarPaths( - "unsolvableinterface", + "unsolvedsuperconstructor2", new String[] {"com/example/Foo.java"}, new String[] {"com.example.Foo#bar()"}); } diff --git a/src/test/java/org/checkerframework/specimin/UnsolvedSuperConstructorTest.java b/src/test/java/org/checkerframework/specimin/UnsolvedSuperConstructorTest.java new file mode 100644 index 000000000..dc860ff9a --- /dev/null +++ b/src/test/java/org/checkerframework/specimin/UnsolvedSuperConstructorTest.java @@ -0,0 +1,19 @@ +package org.checkerframework.specimin; + +import java.io.IOException; +import org.junit.Test; + +/** + * This test checks that if Specimin will produce compilable output if a target method references a + * non-default constructor in the parent class, but also references a constructor in the target + * class which contains a super() call. + */ +public class UnsolvedSuperConstructorTest { + @Test + public void runTest() throws IOException { + SpeciminTestExecutor.runTestWithoutJarPaths( + "unsolvedsuperconstructor", + new String[] {"com/example/Foo.java"}, + new String[] {"com.example.Foo#bar()", "com.example.Foo#Foo()"}); + } +} diff --git a/src/test/resources/ExceptionTypeInferenceImprecision/expected/org/fortest/UnsolvedType.java b/src/test/resources/ExceptionTypeInferenceImprecision/expected/org/fortest/UnsolvedType.java index 37781fd1d..43e298f40 100644 --- a/src/test/resources/ExceptionTypeInferenceImprecision/expected/org/fortest/UnsolvedType.java +++ b/src/test/resources/ExceptionTypeInferenceImprecision/expected/org/fortest/UnsolvedType.java @@ -1,6 +1,6 @@ package org.fortest; -public class UnsolvedType extends java.lang.RuntimeException { +public class UnsolvedType extends java.lang.Error { public UnsolvedType(java.lang.String parameter0) { throw new java.lang.Error(); diff --git a/src/test/resources/MethodInEnum/expected/com/example/Foo.java b/src/test/resources/MethodInEnum/expected/com/example/Foo.java index d8edac7bd..4a139d71b 100644 --- a/src/test/resources/MethodInEnum/expected/com/example/Foo.java +++ b/src/test/resources/MethodInEnum/expected/com/example/Foo.java @@ -7,7 +7,7 @@ private enum STATUS { ON { public void testing() { - throw new RuntimeException(); + throw new java.lang.Error(); } } ; diff --git a/src/test/resources/MethodInEnum/input/com/example/Foo.java b/src/test/resources/MethodInEnum/input/com/example/Foo.java index 65805ca9c..b491538a8 100644 --- a/src/test/resources/MethodInEnum/input/com/example/Foo.java +++ b/src/test/resources/MethodInEnum/input/com/example/Foo.java @@ -4,8 +4,6 @@ class Foo { private enum STATUS{ ON { - // this method should be emptied. Sadly we can't figure out why it is preserved in the final output. - // The good thing is that UnsolvedSymbolVisitor will make sure that everything inside this method resolved. public void testing() { throw new RuntimeException(); } diff --git a/src/test/resources/ObjectCreationInsideAnonymousClass/expected/com/example/Simple.java b/src/test/resources/ObjectCreationInsideAnonymousClass/expected/com/example/Simple.java index d15da0020..71dc488f8 100644 --- a/src/test/resources/ObjectCreationInsideAnonymousClass/expected/com/example/Simple.java +++ b/src/test/resources/ObjectCreationInsideAnonymousClass/expected/com/example/Simple.java @@ -11,8 +11,11 @@ public Baz getBaz() { private Foo foo = new Foo("starting"); public void remove() { + other(); foo.remove(); } + + private void other() {} }; } } diff --git a/src/test/resources/ObjectCreationInsideAnonymousClass/expected/org/testing/Foo.java b/src/test/resources/ObjectCreationInsideAnonymousClass/expected/org/testing/Foo.java index ff6dfb28d..2e85021a7 100644 --- a/src/test/resources/ObjectCreationInsideAnonymousClass/expected/org/testing/Foo.java +++ b/src/test/resources/ObjectCreationInsideAnonymousClass/expected/org/testing/Foo.java @@ -2,11 +2,11 @@ public class Foo { - public Foo(java.lang.String parameter0) { + public org.testing.RemoveReturnType remove() { throw new java.lang.Error(); } - public RemoveReturnType remove() { + public Foo(java.lang.String parameter0) { throw new java.lang.Error(); } } diff --git a/src/test/resources/ObjectCreationInsideAnonymousClass/input/com/example/Simple.java b/src/test/resources/ObjectCreationInsideAnonymousClass/input/com/example/Simple.java index 57a5c5b02..5269b6c63 100644 --- a/src/test/resources/ObjectCreationInsideAnonymousClass/input/com/example/Simple.java +++ b/src/test/resources/ObjectCreationInsideAnonymousClass/input/com/example/Simple.java @@ -5,13 +5,18 @@ public class Simple { - public Baz getBaz (){ + public Baz getBaz() { return new Baz() { private Foo foo = new Foo("starting"); public void remove() { + other(); foo.remove(); } + + private void other() { + + } }; } } diff --git a/src/test/resources/ThrowableImprecision/expected/org/testing/UnsolvedType.java b/src/test/resources/ThrowableImprecision/expected/org/testing/UnsolvedType.java index 6a802e75e..5035e6e72 100644 --- a/src/test/resources/ThrowableImprecision/expected/org/testing/UnsolvedType.java +++ b/src/test/resources/ThrowableImprecision/expected/org/testing/UnsolvedType.java @@ -1,6 +1,6 @@ package org.testing; -public class UnsolvedType extends java.lang.Throwable { +public class UnsolvedType extends java.lang.Exception { public UnsolvedType() { throw new java.lang.Error(); diff --git a/src/test/resources/ThrowableType/expected/org/testing/UnsolvedType.java b/src/test/resources/ThrowableType/expected/org/testing/UnsolvedType.java index 6a802e75e..5035e6e72 100644 --- a/src/test/resources/ThrowableType/expected/org/testing/UnsolvedType.java +++ b/src/test/resources/ThrowableType/expected/org/testing/UnsolvedType.java @@ -1,6 +1,6 @@ package org.testing; -public class UnsolvedType extends java.lang.Throwable { +public class UnsolvedType extends java.lang.Exception { public UnsolvedType() { throw new java.lang.Error(); diff --git a/src/test/resources/abstractimpl/expected/com/example/Simple.java b/src/test/resources/abstractimpl/expected/com/example/Simple.java index 675a0cc5f..656d5ae14 100644 --- a/src/test/resources/abstractimpl/expected/com/example/Simple.java +++ b/src/test/resources/abstractimpl/expected/com/example/Simple.java @@ -1,7 +1,7 @@ package com.example; -import java.util.Set; import java.util.Collection; +import java.util.Set; public class Simple { Collection bar(K key, Collection collection) { diff --git a/src/test/resources/abstractimpl/expected/com/example/WrappedSet.java b/src/test/resources/abstractimpl/expected/com/example/WrappedSet.java index 46a42507c..02d2f6d0c 100644 --- a/src/test/resources/abstractimpl/expected/com/example/WrappedSet.java +++ b/src/test/resources/abstractimpl/expected/com/example/WrappedSet.java @@ -1,8 +1,8 @@ package com.example; -import java.util.Set; -import java.util.Iterator; import java.util.Collection; +import java.util.Iterator; +import java.util.Set; class WrappedSet implements Set { diff --git a/src/test/resources/abstractsuperwithconcreteimpl/expected/com/example/Bar.java b/src/test/resources/abstractsuperwithconcreteimpl/expected/com/example/Bar.java new file mode 100644 index 000000000..362d49f1b --- /dev/null +++ b/src/test/resources/abstractsuperwithconcreteimpl/expected/com/example/Bar.java @@ -0,0 +1,3 @@ +package com.example; + +public class Bar { } diff --git a/src/test/resources/abstractsuperwithconcreteimpl/expected/com/example/Concrete.java b/src/test/resources/abstractsuperwithconcreteimpl/expected/com/example/Concrete.java new file mode 100644 index 000000000..0182bb455 --- /dev/null +++ b/src/test/resources/abstractsuperwithconcreteimpl/expected/com/example/Concrete.java @@ -0,0 +1,11 @@ +package com.example; + +public class Concrete implements Super { + public Foo foo() { + throw new java.lang.Error(); + } + + public Bar bar() { + throw new java.lang.Error(); + } +} diff --git a/src/test/resources/abstractsuperwithconcreteimpl/expected/com/example/Foo.java b/src/test/resources/abstractsuperwithconcreteimpl/expected/com/example/Foo.java new file mode 100644 index 000000000..257fc5e79 --- /dev/null +++ b/src/test/resources/abstractsuperwithconcreteimpl/expected/com/example/Foo.java @@ -0,0 +1,3 @@ +package com.example; + +public class Foo { } diff --git a/src/test/resources/abstractsuperwithconcreteimpl/expected/com/example/Simple.java b/src/test/resources/abstractsuperwithconcreteimpl/expected/com/example/Simple.java new file mode 100644 index 000000000..240e6c936 --- /dev/null +++ b/src/test/resources/abstractsuperwithconcreteimpl/expected/com/example/Simple.java @@ -0,0 +1,10 @@ +package com.example; + +public class Simple { + private Super s; + public void foo() { + s.foo(); + Concrete concrete = new Concrete(); + s.bar(); + } +} diff --git a/src/test/resources/abstractsuperwithconcreteimpl/expected/com/example/Super.java b/src/test/resources/abstractsuperwithconcreteimpl/expected/com/example/Super.java new file mode 100644 index 000000000..dc6cbf1cb --- /dev/null +++ b/src/test/resources/abstractsuperwithconcreteimpl/expected/com/example/Super.java @@ -0,0 +1,6 @@ +package com.example; + +public interface Super { + Foo foo(); + Bar bar(); +} diff --git a/src/test/resources/abstractsuperwithconcreteimpl/input/com/example/Concrete.java b/src/test/resources/abstractsuperwithconcreteimpl/input/com/example/Concrete.java new file mode 100644 index 000000000..c6a3a6459 --- /dev/null +++ b/src/test/resources/abstractsuperwithconcreteimpl/input/com/example/Concrete.java @@ -0,0 +1,9 @@ +package com.example; + +public class Concrete implements Super { + public Foo foo() { + } + + public Bar bar() { + } +} diff --git a/src/test/resources/abstractsuperwithconcreteimpl/input/com/example/Simple.java b/src/test/resources/abstractsuperwithconcreteimpl/input/com/example/Simple.java new file mode 100644 index 000000000..5103a2fb6 --- /dev/null +++ b/src/test/resources/abstractsuperwithconcreteimpl/input/com/example/Simple.java @@ -0,0 +1,11 @@ +package com.example; + +public class Simple { + private Super s; + public void foo() { + // Both foo and bar should be preserved in Super and Concrete + s.foo(); + Concrete concrete = new Concrete(); + s.bar(); + } +} diff --git a/src/test/resources/abstractsuperwithconcreteimpl/input/com/example/Super.java b/src/test/resources/abstractsuperwithconcreteimpl/input/com/example/Super.java new file mode 100644 index 000000000..dc6cbf1cb --- /dev/null +++ b/src/test/resources/abstractsuperwithconcreteimpl/input/com/example/Super.java @@ -0,0 +1,6 @@ +package com.example; + +public interface Super { + Foo foo(); + Bar bar(); +} diff --git a/src/test/resources/annoingenericarg/expected/org/checkerframework/checker/nullness/KeyFor.java b/src/test/resources/annoingenericarg/expected/org/checkerframework/checker/nullness/KeyFor.java index 54252d438..23ac512a5 100644 --- a/src/test/resources/annoingenericarg/expected/org/checkerframework/checker/nullness/KeyFor.java +++ b/src/test/resources/annoingenericarg/expected/org/checkerframework/checker/nullness/KeyFor.java @@ -1,7 +1,7 @@ package org.checkerframework.checker.nullness; -@java.lang.annotation.Target({ java.lang.annotation.ElementType.TYPE_USE }) +@java.lang.annotation.Target({ java.lang.annotation.ElementType.TYPE_USE, java.lang.annotation.ElementType.TYPE, java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.PARAMETER, java.lang.annotation.ElementType.CONSTRUCTOR, java.lang.annotation.ElementType.LOCAL_VARIABLE, java.lang.annotation.ElementType.ANNOTATION_TYPE, java.lang.annotation.ElementType.PACKAGE, java.lang.annotation.ElementType.TYPE_PARAMETER}) public @interface KeyFor { - public String value(); + public java.lang.String value(); } diff --git a/src/test/resources/annoingenerictarget/expected/com/example/Simple.java b/src/test/resources/annoingenerictarget/expected/com/example/Simple.java index 417a8fd1e..380e48b05 100644 --- a/src/test/resources/annoingenerictarget/expected/com/example/Simple.java +++ b/src/test/resources/annoingenerictarget/expected/com/example/Simple.java @@ -1,15 +1,18 @@ package com.example; import java.util.Collection; -import org.checkerframework.checker.nullness.qual.*; import org.checkerframework.checker.initialization.qual.Initialized; +import org.checkerframework.checker.nullness.qual.*; class Simple { - @Initialized - @NonNull - @UnknownKeyFor - public static Collection unmodifiableCollection(@Initialized @NonNull @UnknownKeyFor Collection<@Initialized @KeyForBottom @NonNull ? extends T> c) { - return null; - } + @Initialized + @NonNull + @UnknownKeyFor + public static + Collection unmodifiableCollection( + @Initialized @NonNull @UnknownKeyFor + Collection<@Initialized @KeyForBottom @NonNull ? extends T> c) { + return null; + } } diff --git a/src/test/resources/annoingenerictarget/expected/org/checkerframework/checker/initialization/qual/Initialized.java b/src/test/resources/annoingenerictarget/expected/org/checkerframework/checker/initialization/qual/Initialized.java index f0b654af6..728a4e6cb 100644 --- a/src/test/resources/annoingenerictarget/expected/org/checkerframework/checker/initialization/qual/Initialized.java +++ b/src/test/resources/annoingenerictarget/expected/org/checkerframework/checker/initialization/qual/Initialized.java @@ -1,5 +1,5 @@ package org.checkerframework.checker.initialization.qual; -@java.lang.annotation.Target({ java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.PARAMETER, java.lang.annotation.ElementType.TYPE_USE }) +@java.lang.annotation.Target({ java.lang.annotation.ElementType.TYPE_USE, java.lang.annotation.ElementType.TYPE, java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.PARAMETER, java.lang.annotation.ElementType.CONSTRUCTOR, java.lang.annotation.ElementType.LOCAL_VARIABLE, java.lang.annotation.ElementType.ANNOTATION_TYPE, java.lang.annotation.ElementType.PACKAGE, java.lang.annotation.ElementType.TYPE_PARAMETER}) public @interface Initialized { } diff --git a/src/test/resources/annoingenerictarget/expected/org/checkerframework/checker/nullness/qual/KeyForBottom.java b/src/test/resources/annoingenerictarget/expected/org/checkerframework/checker/nullness/qual/KeyForBottom.java index 8a3362c74..773d8311c 100644 --- a/src/test/resources/annoingenerictarget/expected/org/checkerframework/checker/nullness/qual/KeyForBottom.java +++ b/src/test/resources/annoingenerictarget/expected/org/checkerframework/checker/nullness/qual/KeyForBottom.java @@ -1,5 +1,5 @@ package org.checkerframework.checker.nullness.qual; -@java.lang.annotation.Target({ java.lang.annotation.ElementType.TYPE_USE }) +@java.lang.annotation.Target({ java.lang.annotation.ElementType.TYPE_USE, java.lang.annotation.ElementType.TYPE, java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.PARAMETER, java.lang.annotation.ElementType.CONSTRUCTOR, java.lang.annotation.ElementType.LOCAL_VARIABLE, java.lang.annotation.ElementType.ANNOTATION_TYPE, java.lang.annotation.ElementType.PACKAGE, java.lang.annotation.ElementType.TYPE_PARAMETER}) public @interface KeyForBottom { } diff --git a/src/test/resources/annoingenerictarget/expected/org/checkerframework/checker/nullness/qual/NonNull.java b/src/test/resources/annoingenerictarget/expected/org/checkerframework/checker/nullness/qual/NonNull.java index 9b23b52a0..9886a1795 100644 --- a/src/test/resources/annoingenerictarget/expected/org/checkerframework/checker/nullness/qual/NonNull.java +++ b/src/test/resources/annoingenerictarget/expected/org/checkerframework/checker/nullness/qual/NonNull.java @@ -1,5 +1,5 @@ package org.checkerframework.checker.nullness.qual; -@java.lang.annotation.Target({ java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.PARAMETER, java.lang.annotation.ElementType.TYPE_USE }) +@java.lang.annotation.Target({ java.lang.annotation.ElementType.TYPE_USE, java.lang.annotation.ElementType.TYPE, java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.PARAMETER, java.lang.annotation.ElementType.CONSTRUCTOR, java.lang.annotation.ElementType.LOCAL_VARIABLE, java.lang.annotation.ElementType.ANNOTATION_TYPE, java.lang.annotation.ElementType.PACKAGE, java.lang.annotation.ElementType.TYPE_PARAMETER}) public @interface NonNull { } diff --git a/src/test/resources/annoingenerictarget/expected/org/checkerframework/checker/nullness/qual/Nullable.java b/src/test/resources/annoingenerictarget/expected/org/checkerframework/checker/nullness/qual/Nullable.java index ec98e5864..104a5a361 100644 --- a/src/test/resources/annoingenerictarget/expected/org/checkerframework/checker/nullness/qual/Nullable.java +++ b/src/test/resources/annoingenerictarget/expected/org/checkerframework/checker/nullness/qual/Nullable.java @@ -1,5 +1,5 @@ package org.checkerframework.checker.nullness.qual; -@java.lang.annotation.Target({ java.lang.annotation.ElementType.TYPE_USE }) +@java.lang.annotation.Target({ java.lang.annotation.ElementType.TYPE_USE, java.lang.annotation.ElementType.TYPE, java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.PARAMETER, java.lang.annotation.ElementType.CONSTRUCTOR, java.lang.annotation.ElementType.LOCAL_VARIABLE, java.lang.annotation.ElementType.ANNOTATION_TYPE, java.lang.annotation.ElementType.PACKAGE, java.lang.annotation.ElementType.TYPE_PARAMETER}) public @interface Nullable { } diff --git a/src/test/resources/annoingenerictarget/expected/org/checkerframework/checker/nullness/qual/UnknownKeyFor.java b/src/test/resources/annoingenerictarget/expected/org/checkerframework/checker/nullness/qual/UnknownKeyFor.java index 9bbb7d0bc..712e9379c 100644 --- a/src/test/resources/annoingenerictarget/expected/org/checkerframework/checker/nullness/qual/UnknownKeyFor.java +++ b/src/test/resources/annoingenerictarget/expected/org/checkerframework/checker/nullness/qual/UnknownKeyFor.java @@ -1,5 +1,5 @@ package org.checkerframework.checker.nullness.qual; -@java.lang.annotation.Target({ java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.PARAMETER, java.lang.annotation.ElementType.TYPE_USE }) +@java.lang.annotation.Target({ java.lang.annotation.ElementType.TYPE_USE, java.lang.annotation.ElementType.TYPE, java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.PARAMETER, java.lang.annotation.ElementType.CONSTRUCTOR, java.lang.annotation.ElementType.LOCAL_VARIABLE, java.lang.annotation.ElementType.ANNOTATION_TYPE, java.lang.annotation.ElementType.PACKAGE, java.lang.annotation.ElementType.TYPE_PARAMETER}) public @interface UnknownKeyFor { } diff --git a/src/test/resources/booleanexpr/expected/com/example/Foo.java b/src/test/resources/booleanexpr/expected/com/example/Foo.java index d520b0fdd..6cac8a882 100644 --- a/src/test/resources/booleanexpr/expected/com/example/Foo.java +++ b/src/test/resources/booleanexpr/expected/com/example/Foo.java @@ -6,27 +6,27 @@ public class Foo { public boolean isBaz; - public boolean isBar() { + public java.lang.Long getBigLong() { throw new java.lang.Error(); } - public int qux() { + public long getLong() { throw new java.lang.Error(); } - public double razz() { + public int getX() { throw new java.lang.Error(); } - public int getX() { + public double razz() { throw new java.lang.Error(); } - public long getLong() { + public int qux() { throw new java.lang.Error(); } - public Long getBigLong() { + public boolean isBar() { throw new java.lang.Error(); } -} \ No newline at end of file +} diff --git a/src/test/resources/ctorargument/expected/com/example/Simple.java b/src/test/resources/ctorargument/expected/com/example/Simple.java index 3dd6b9676..e93b25cd4 100644 --- a/src/test/resources/ctorargument/expected/com/example/Simple.java +++ b/src/test/resources/ctorargument/expected/com/example/Simple.java @@ -1,8 +1,8 @@ package com.example; import org.example.MethodGen; -import org.example.VerifierConstraintViolatedException; import org.example.VerificationResult; +import org.example.VerifierConstraintViolatedException; public class Simple { diff --git a/src/test/resources/ctorargument/expected/org/example/VerificationResult.java b/src/test/resources/ctorargument/expected/org/example/VerificationResult.java index 82cebb972..12a2932b5 100644 --- a/src/test/resources/ctorargument/expected/org/example/VerificationResult.java +++ b/src/test/resources/ctorargument/expected/org/example/VerificationResult.java @@ -1,10 +1,10 @@ package org.example; public class VerificationResult { + public static org.example.VerificationResult OK; - public static OrgExampleVerificationResultVERIFIED_REJECTEDSyntheticType VERIFIED_REJECTED; + public static org.example.OrgExampleVerificationResultVERIFIED_REJECTEDSyntheticType VERIFIED_REJECTED; - public static org.example.VerificationResult OK; public VerificationResult(org.example.OrgExampleVerificationResultVERIFIED_REJECTEDSyntheticType parameter0, java.lang.String parameter1) { throw new java.lang.Error(); diff --git a/src/test/resources/ctorargument/expected/org/example/VerifierConstraintViolatedException.java b/src/test/resources/ctorargument/expected/org/example/VerifierConstraintViolatedException.java index 0726294af..ac8638dd1 100644 --- a/src/test/resources/ctorargument/expected/org/example/VerifierConstraintViolatedException.java +++ b/src/test/resources/ctorargument/expected/org/example/VerifierConstraintViolatedException.java @@ -1,8 +1,8 @@ package org.example; -public class VerifierConstraintViolatedException extends Exception { +public class VerifierConstraintViolatedException extends java.lang.Exception { - public ExtendMessageReturnType extendMessage(java.lang.String parameter0, java.lang.String parameter1) { + public org.example.ExtendMessageReturnType extendMessage(java.lang.String parameter0, java.lang.String parameter1) { throw new java.lang.Error(); } } diff --git a/src/test/resources/customexception/expected/com/example/CustomException.java b/src/test/resources/customexception/expected/com/example/CustomException.java index 057fb5766..d35372bca 100644 --- a/src/test/resources/customexception/expected/com/example/CustomException.java +++ b/src/test/resources/customexception/expected/com/example/CustomException.java @@ -1,6 +1,6 @@ package com.example; -public class CustomException extends Exception { +public class CustomException extends java.lang.Exception { public CustomException(String msg) { throw new java.lang.Error(); diff --git a/src/test/resources/customexception/input/com/example/CustomException.java b/src/test/resources/customexception/input/com/example/CustomException.java index a0fcf0e3f..8111dc268 100644 --- a/src/test/resources/customexception/input/com/example/CustomException.java +++ b/src/test/resources/customexception/input/com/example/CustomException.java @@ -1,5 +1,5 @@ package com.example; -public class CustomException extends Exception { +public class CustomException extends java.lang.Exception { public CustomException (String msg) { } } diff --git a/src/test/resources/enumconstantarg/expected/org/example/Op.java b/src/test/resources/enumconstantarg/expected/org/example/Op.java index 717f4766f..14c917f85 100644 --- a/src/test/resources/enumconstantarg/expected/org/example/Op.java +++ b/src/test/resources/enumconstantarg/expected/org/example/Op.java @@ -2,9 +2,5 @@ public class Op { - public static org.example.Op CONTAINS; - public static org.example.Op EQ; - - public static org.example.Op NOT_EQ; } \ No newline at end of file diff --git a/src/test/resources/enumwithimplements/expected/com/example/Function.java b/src/test/resources/enumwithimplements/expected/com/example/Function.java new file mode 100644 index 000000000..1a80c5640 --- /dev/null +++ b/src/test/resources/enumwithimplements/expected/com/example/Function.java @@ -0,0 +1,3 @@ +package com.example; + +public interface Function {} diff --git a/src/test/resources/enumwithimplements/expected/com/example/Simple.java b/src/test/resources/enumwithimplements/expected/com/example/Simple.java index b63c59523..78dc84b73 100644 --- a/src/test/resources/enumwithimplements/expected/com/example/Simple.java +++ b/src/test/resources/enumwithimplements/expected/com/example/Simple.java @@ -1,13 +1,14 @@ package com.example; public class Simple { - private enum MyEnum { - A, B - } + private enum MyEnum implements Function { + A, + B + } - void bar() { - MyEnum a = MyEnum.A; - MyEnum b = MyEnum.B; - } + void bar() { + MyEnum a = MyEnum.A; + MyEnum b = MyEnum.B; + } } diff --git a/src/test/resources/enumwithimplements/input/com/example/Function.java b/src/test/resources/enumwithimplements/input/com/example/Function.java index b845be827..5dcae3335 100644 --- a/src/test/resources/enumwithimplements/input/com/example/Function.java +++ b/src/test/resources/enumwithimplements/input/com/example/Function.java @@ -1,4 +1,5 @@ package com.example; public interface Function { + void foo(); } \ No newline at end of file diff --git a/src/test/resources/enumwithimplements/input/com/example/Simple.java b/src/test/resources/enumwithimplements/input/com/example/Simple.java index f08f09011..5ff39fe13 100644 --- a/src/test/resources/enumwithimplements/input/com/example/Simple.java +++ b/src/test/resources/enumwithimplements/input/com/example/Simple.java @@ -3,11 +3,15 @@ import com.example.Function; public class Simple { - private enum MyEnum implements Function { - A, B + private enum MyEnum implements Function { + A, B; + + public void foo() { + + } } - // target. Goal of this test is to make sure that Function, above, is not created/preserved. + // target. void bar() { MyEnum a = MyEnum.A; MyEnum b = MyEnum.B; diff --git a/src/test/resources/enumwithimplements2/expected/com/example/Foo.java b/src/test/resources/enumwithimplements2/expected/com/example/Foo.java new file mode 100644 index 000000000..245d55ced --- /dev/null +++ b/src/test/resources/enumwithimplements2/expected/com/example/Foo.java @@ -0,0 +1,5 @@ +package com.example; + +public interface Foo { + +} diff --git a/src/test/resources/enumwithimplements2/expected/com/example/Simple.java b/src/test/resources/enumwithimplements2/expected/com/example/Simple.java index b63c59523..37c4cca60 100644 --- a/src/test/resources/enumwithimplements2/expected/com/example/Simple.java +++ b/src/test/resources/enumwithimplements2/expected/com/example/Simple.java @@ -1,9 +1,9 @@ package com.example; public class Simple { - private enum MyEnum { - - A, B + private enum MyEnum implements Foo { + A, + B } void bar() { diff --git a/src/test/resources/enumwithimplements2/input/com/example/Simple.java b/src/test/resources/enumwithimplements2/input/com/example/Simple.java index f08f09011..5ea0ed562 100644 --- a/src/test/resources/enumwithimplements2/input/com/example/Simple.java +++ b/src/test/resources/enumwithimplements2/input/com/example/Simple.java @@ -3,7 +3,7 @@ import com.example.Function; public class Simple { - private enum MyEnum implements Function { + private enum MyEnum implements Foo { A, B } diff --git a/src/test/resources/extendqualifiedpath/expected/com/example/Baz.java b/src/test/resources/extendqualifiedpath/expected/com/example/Baz.java index 0c26b854d..eb3680f26 100644 --- a/src/test/resources/extendqualifiedpath/expected/com/example/Baz.java +++ b/src/test/resources/extendqualifiedpath/expected/com/example/Baz.java @@ -2,7 +2,5 @@ public interface Baz extends org.testing.Baz { - default void bar() { - throw new java.lang.Error(); - } + void bar(); } diff --git a/src/test/resources/extendswithtypevar/expected/com/example/AbstractMapEntry.java b/src/test/resources/extendswithtypevar/expected/com/example/AbstractMapEntry.java index 361d55f62..ab4808a29 100644 --- a/src/test/resources/extendswithtypevar/expected/com/example/AbstractMapEntry.java +++ b/src/test/resources/extendswithtypevar/expected/com/example/AbstractMapEntry.java @@ -1,6 +1,8 @@ package com.example; -abstract class AbstractMapEntry { +import java.util.Map.Entry; + +abstract class AbstractMapEntry implements Entry { public V setValue(V value) { throw new java.lang.Error(); diff --git a/src/test/resources/finalfieldwithnontargetconstructor/expected/com/example/Foo.java b/src/test/resources/finalfieldwithnontargetconstructor/expected/com/example/Foo.java new file mode 100644 index 000000000..e218ce930 --- /dev/null +++ b/src/test/resources/finalfieldwithnontargetconstructor/expected/com/example/Foo.java @@ -0,0 +1,9 @@ +package com.example; + +public class Foo { + private final String bar = null; + + public String bar() { + return bar; + } +} diff --git a/src/test/resources/finalfieldwithnontargetconstructor/input/com/example/Foo.java b/src/test/resources/finalfieldwithnontargetconstructor/input/com/example/Foo.java new file mode 100644 index 000000000..67d99a138 --- /dev/null +++ b/src/test/resources/finalfieldwithnontargetconstructor/input/com/example/Foo.java @@ -0,0 +1,13 @@ +package com.example; + +public class Foo { + private final String bar; + + public Foo(String bar) { + this.bar = bar; + } + + public String bar() { + return bar; + } +} diff --git a/src/test/resources/foreachlocal/expected/com/example/Foo.java b/src/test/resources/foreachlocal/expected/com/example/Foo.java index 3736aa07b..4573a375a 100644 --- a/src/test/resources/foreachlocal/expected/com/example/Foo.java +++ b/src/test/resources/foreachlocal/expected/com/example/Foo.java @@ -2,7 +2,7 @@ public class Foo { - public DoSomethingReturnType doSomething() { + public com.example.DoSomethingReturnType doSomething() { throw new java.lang.Error(); } } \ No newline at end of file diff --git a/src/test/resources/hiddenType/expected/com/github/javaparser/ast/body/MethodDeclaration.java b/src/test/resources/hiddenType/expected/com/github/javaparser/ast/body/MethodDeclaration.java index 8a1a7475e..4a4565aa5 100644 --- a/src/test/resources/hiddenType/expected/com/github/javaparser/ast/body/MethodDeclaration.java +++ b/src/test/resources/hiddenType/expected/com/github/javaparser/ast/body/MethodDeclaration.java @@ -1,7 +1,7 @@ package com.github.javaparser.ast.body; public class MethodDeclaration { - public GetTypeReturnType getType() { + public com.github.javaparser.ast.body.GetTypeReturnType getType() { throw new java.lang.Error(); } } diff --git a/src/test/resources/implicitinterfaceaccess/expected/org/testing/B.java b/src/test/resources/implicitinterfaceaccess/expected/org/testing/B.java index b2830efcf..cfd1e9d33 100644 --- a/src/test/resources/implicitinterfaceaccess/expected/org/testing/B.java +++ b/src/test/resources/implicitinterfaceaccess/expected/org/testing/B.java @@ -2,7 +2,5 @@ public interface B { - public default int baz() { - throw new java.lang.Error(); - } + public int baz(); } diff --git a/src/test/resources/implicitinterfaceaccessmethodoverload/expected/org/testing/B.java b/src/test/resources/implicitinterfaceaccessmethodoverload/expected/org/testing/B.java index aa20d3dcc..5a134a672 100644 --- a/src/test/resources/implicitinterfaceaccessmethodoverload/expected/org/testing/B.java +++ b/src/test/resources/implicitinterfaceaccessmethodoverload/expected/org/testing/B.java @@ -2,7 +2,5 @@ public interface B { - public default FooReturnType foo(int parameter0, java.lang.String parameter1) { - throw new java.lang.Error(); - } + public org.testing.FooReturnType foo(int parameter0, java.lang.String parameter1); } diff --git a/src/test/resources/implicitinterfaceaccesswithmanyinterfaces/expected/org/testing/B.java b/src/test/resources/implicitinterfaceaccesswithmanyinterfaces/expected/org/testing/B.java index 43849b282..cfd1e9d33 100644 --- a/src/test/resources/implicitinterfaceaccesswithmanyinterfaces/expected/org/testing/B.java +++ b/src/test/resources/implicitinterfaceaccesswithmanyinterfaces/expected/org/testing/B.java @@ -1,4 +1,6 @@ package org.testing; public interface B { + + public int baz(); } diff --git a/src/test/resources/implicitinterfaceaccesswithmanyinterfaces/expected/org/testing/D.java b/src/test/resources/implicitinterfaceaccesswithmanyinterfaces/expected/org/testing/D.java index 79ab13bfc..94dc5489b 100644 --- a/src/test/resources/implicitinterfaceaccesswithmanyinterfaces/expected/org/testing/D.java +++ b/src/test/resources/implicitinterfaceaccesswithmanyinterfaces/expected/org/testing/D.java @@ -1,8 +1,4 @@ package org.testing; public interface D { - - public default int baz() { - throw new java.lang.Error(); - } } diff --git a/src/test/resources/importanno/expected/com/example/Simple.java b/src/test/resources/importanno/expected/com/example/Simple.java index 3c3d3d864..d1ba6e02a 100644 --- a/src/test/resources/importanno/expected/com/example/Simple.java +++ b/src/test/resources/importanno/expected/com/example/Simple.java @@ -1,7 +1,7 @@ package com.example; -import org.checkerframework.checker.mustcall.qual.Owning; import java.net.Socket; +import org.checkerframework.checker.mustcall.qual.Owning; public class Simple { diff --git a/src/test/resources/importanno/expected/org/checkerframework/checker/mustcall/qual/Owning.java b/src/test/resources/importanno/expected/org/checkerframework/checker/mustcall/qual/Owning.java index 463af1eee..d1c3e2d88 100644 --- a/src/test/resources/importanno/expected/org/checkerframework/checker/mustcall/qual/Owning.java +++ b/src/test/resources/importanno/expected/org/checkerframework/checker/mustcall/qual/Owning.java @@ -6,6 +6,6 @@ import java.lang.annotation.Target; @Retention(RetentionPolicy.RUNTIME) -@Target({ ElementType.FIELD }) +@Target({ ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD }) public @interface Owning { } diff --git a/src/test/resources/inheritmethodexception/expected/com/example/Parent.java b/src/test/resources/inheritmethodexception/expected/com/example/Parent.java index bd88b5a45..7db224677 100644 --- a/src/test/resources/inheritmethodexception/expected/com/example/Parent.java +++ b/src/test/resources/inheritmethodexception/expected/com/example/Parent.java @@ -2,7 +2,7 @@ public class Parent { - public void foo() throws UnknownException { + public void foo() throws com.example.UnknownException { throw new java.lang.Error(); } } diff --git a/src/test/resources/inheritmethodexception/expected/com/example/UnknownException.java b/src/test/resources/inheritmethodexception/expected/com/example/UnknownException.java index c3b118463..ef1e6cfc2 100644 --- a/src/test/resources/inheritmethodexception/expected/com/example/UnknownException.java +++ b/src/test/resources/inheritmethodexception/expected/com/example/UnknownException.java @@ -1,4 +1,4 @@ package com.example; -public class UnknownException extends java.lang.Throwable { +public class UnknownException extends java.lang.Exception { } diff --git a/src/test/resources/innerclasstypecorrect/expected/com/example/Simple.java b/src/test/resources/innerclasstypecorrect/expected/com/example/Simple.java index 601c3b919..a865e1bab 100644 --- a/src/test/resources/innerclasstypecorrect/expected/com/example/Simple.java +++ b/src/test/resources/innerclasstypecorrect/expected/com/example/Simple.java @@ -1,7 +1,7 @@ package com.example; -import org.example.Outer; import org.example.Other; +import org.example.Outer; public class Simple { public Other bar() { diff --git a/src/test/resources/interfacechain/expected/com/example/Foo.java b/src/test/resources/interfacechain/expected/com/example/Foo.java index ad9a42d3d..912add98a 100644 --- a/src/test/resources/interfacechain/expected/com/example/Foo.java +++ b/src/test/resources/interfacechain/expected/com/example/Foo.java @@ -23,12 +23,9 @@ public boolean hasNext() { } } - public interface Iterator3 extends Iterator2 { - } + public interface Iterator3 extends Iterator2 {} - public interface Iterator2 extends Iterator1 { - } + public interface Iterator2 extends Iterator1 {} - public interface Iterator1 extends Iterator { - } + public interface Iterator1 extends Iterator {} } diff --git a/src/test/resources/interfaceimplemented/expected/com/example/Baz.java b/src/test/resources/interfaceimplemented/expected/com/example/Baz.java index 763d63d06..583e44fa6 100644 --- a/src/test/resources/interfaceimplemented/expected/com/example/Baz.java +++ b/src/test/resources/interfaceimplemented/expected/com/example/Baz.java @@ -2,7 +2,5 @@ public interface Baz { - default void doSomething() { - throw new java.lang.Error(); - } + void doSomething(); } diff --git a/src/test/resources/interfacemethodwithunsolvedtype/expected/com/example/Baz.java b/src/test/resources/interfacemethodwithunsolvedtype/expected/com/example/Baz.java index ce95b991e..91f5dad28 100644 --- a/src/test/resources/interfacemethodwithunsolvedtype/expected/com/example/Baz.java +++ b/src/test/resources/interfacemethodwithunsolvedtype/expected/com/example/Baz.java @@ -4,7 +4,5 @@ public interface Baz { - default UnsolvedType doSomething(T value) { - throw new java.lang.Error(); - } + UnsolvedType doSomething(T value); } diff --git a/src/test/resources/interfaceusecomplex/expected/com/example/Baz.java b/src/test/resources/interfaceusecomplex/expected/com/example/Baz.java index d93b1d96d..643729763 100644 --- a/src/test/resources/interfaceusecomplex/expected/com/example/Baz.java +++ b/src/test/resources/interfaceusecomplex/expected/com/example/Baz.java @@ -1,7 +1,5 @@ package com.example; -public interface Baz { - public default boolean containsAll(com.example.Baz parameter0) { - throw new java.lang.Error(); - } +public interface Baz { + public boolean containsAll(com.example.Baz parameter0); } diff --git a/src/test/resources/interfacewithgenerictype/expected/com/example/Baz.java b/src/test/resources/interfacewithgenerictype/expected/com/example/Baz.java index 293459438..801318cdc 100644 --- a/src/test/resources/interfacewithgenerictype/expected/com/example/Baz.java +++ b/src/test/resources/interfacewithgenerictype/expected/com/example/Baz.java @@ -2,11 +2,5 @@ public interface Baz { - default void doSomething(T value) { - throw new java.lang.Error(); - } - - default void doSomething(int x) { - throw new java.lang.Error(); - } + void doSomething(T value); } diff --git a/src/test/resources/interfacewithgenerictype/input/com/example/Baz.java b/src/test/resources/interfacewithgenerictype/input/com/example/Baz.java index fedfe554b..b29a10e14 100644 --- a/src/test/resources/interfacewithgenerictype/input/com/example/Baz.java +++ b/src/test/resources/interfacewithgenerictype/input/com/example/Baz.java @@ -5,7 +5,6 @@ public interface Baz { void doSomething(T value); // this method will be removed. void doSomething(); - // sadly we can't remove this method. void doSomething(int x); } diff --git a/src/test/resources/interfacewithunsolvedsymbols/expected/com/example/Baz.java b/src/test/resources/interfacewithunsolvedsymbols/expected/com/example/Baz.java index 293459438..801318cdc 100644 --- a/src/test/resources/interfacewithunsolvedsymbols/expected/com/example/Baz.java +++ b/src/test/resources/interfacewithunsolvedsymbols/expected/com/example/Baz.java @@ -2,11 +2,5 @@ public interface Baz { - default void doSomething(T value) { - throw new java.lang.Error(); - } - - default void doSomething(int x) { - throw new java.lang.Error(); - } + void doSomething(T value); } diff --git a/src/test/resources/issue272/expected/com/example/PostconditionAnnotation.java b/src/test/resources/issue272/expected/com/example/PostconditionAnnotation.java index 55c48127d..7a846c2fb 100644 --- a/src/test/resources/issue272/expected/com/example/PostconditionAnnotation.java +++ b/src/test/resources/issue272/expected/com/example/PostconditionAnnotation.java @@ -1,5 +1,5 @@ package com.example; -@java.lang.annotation.Target({}) +@java.lang.annotation.Target({ java.lang.annotation.ElementType.TYPE_USE, java.lang.annotation.ElementType.TYPE, java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.PARAMETER, java.lang.annotation.ElementType.CONSTRUCTOR, java.lang.annotation.ElementType.LOCAL_VARIABLE, java.lang.annotation.ElementType.ANNOTATION_TYPE, java.lang.annotation.ElementType.PACKAGE, java.lang.annotation.ElementType.TYPE_PARAMETER}) public @interface PostconditionAnnotation { -} +} \ No newline at end of file diff --git a/src/test/resources/issue272/expected/com/example/PreconditionAnnotation.java b/src/test/resources/issue272/expected/com/example/PreconditionAnnotation.java index c4ec35f7f..02f769474 100644 --- a/src/test/resources/issue272/expected/com/example/PreconditionAnnotation.java +++ b/src/test/resources/issue272/expected/com/example/PreconditionAnnotation.java @@ -1,5 +1,5 @@ package com.example; -@java.lang.annotation.Target({}) +@java.lang.annotation.Target({ java.lang.annotation.ElementType.TYPE_USE, java.lang.annotation.ElementType.TYPE, java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.PARAMETER, java.lang.annotation.ElementType.CONSTRUCTOR, java.lang.annotation.ElementType.LOCAL_VARIABLE, java.lang.annotation.ElementType.ANNOTATION_TYPE, java.lang.annotation.ElementType.PACKAGE, java.lang.annotation.ElementType.TYPE_PARAMETER}) public @interface PreconditionAnnotation { } diff --git a/src/test/resources/iteratorexample/expected/com/example/Map.java b/src/test/resources/iteratorexample/expected/com/example/Map.java index c916e11f9..39f399def 100644 --- a/src/test/resources/iteratorexample/expected/com/example/Map.java +++ b/src/test/resources/iteratorexample/expected/com/example/Map.java @@ -2,6 +2,6 @@ public class Map { - public static class Entry { + public static class Entry { } } diff --git a/src/test/resources/lambdabifunction/expected/org/example/LambdaUser.java b/src/test/resources/lambdabifunction/expected/org/example/LambdaUser.java index d77f318a5..53a641765 100644 --- a/src/test/resources/lambdabifunction/expected/org/example/LambdaUser.java +++ b/src/test/resources/lambdabifunction/expected/org/example/LambdaUser.java @@ -1,7 +1,7 @@ package org.example; public class LambdaUser { - public UseReturnType use(java.util.function.BiFunction parameter0) { + public org.example.UseReturnType use(java.util.function.BiFunction parameter0) { throw new java.lang.Error(); } } \ No newline at end of file diff --git a/src/test/resources/lambdabody/expected/com/example/Simple.java b/src/test/resources/lambdabody/expected/com/example/Simple.java index ebffb7e2f..c0f307212 100644 --- a/src/test/resources/lambdabody/expected/com/example/Simple.java +++ b/src/test/resources/lambdabody/expected/com/example/Simple.java @@ -1,13 +1,15 @@ package com.example; +import static com.example.util.Cast.castNonNull; + import com.example.sql.SqlNode; import com.example.sql.SqlParserPos; import com.example.util.Util; -import static com.example.util.Cast.castNonNull; class Simple { private static Iterable toPos(Iterable nodes) { - return Util.transform(nodes, node -> node == null ? castNonNull(null) : node.getParserPosition()); + return Util.transform( + nodes, node -> node == null ? castNonNull(null) : node.getParserPosition()); } } diff --git a/src/test/resources/lambdabodystaticunsolved/expected/com/example/Simple.java b/src/test/resources/lambdabodystaticunsolved/expected/com/example/Simple.java index 4049c1737..aa66ba023 100644 --- a/src/test/resources/lambdabodystaticunsolved/expected/com/example/Simple.java +++ b/src/test/resources/lambdabodystaticunsolved/expected/com/example/Simple.java @@ -1,14 +1,15 @@ package com.example; +import static com.example.nullness.Nullness.castNonNull; + import com.example.sql.SqlNode; import com.example.sql.SqlParserPos; import com.example.util.Util; -import static com.example.nullness.Nullness.castNonNull; - class Simple { private static Iterable toPos(Iterable nodes) { - return Util.transform(nodes, node -> node == null ? castNonNull(null) : node.getParserPosition()); + return Util.transform( + nodes, node -> node == null ? castNonNull(null) : node.getParserPosition()); } } diff --git a/src/test/resources/lambdabodystaticunsolved2/expected/com/example/Simple.java b/src/test/resources/lambdabodystaticunsolved2/expected/com/example/Simple.java index 4049c1737..aa66ba023 100644 --- a/src/test/resources/lambdabodystaticunsolved2/expected/com/example/Simple.java +++ b/src/test/resources/lambdabodystaticunsolved2/expected/com/example/Simple.java @@ -1,14 +1,15 @@ package com.example; +import static com.example.nullness.Nullness.castNonNull; + import com.example.sql.SqlNode; import com.example.sql.SqlParserPos; import com.example.util.Util; -import static com.example.nullness.Nullness.castNonNull; - class Simple { private static Iterable toPos(Iterable nodes) { - return Util.transform(nodes, node -> node == null ? castNonNull(null) : node.getParserPosition()); + return Util.transform( + nodes, node -> node == null ? castNonNull(null) : node.getParserPosition()); } } diff --git a/src/test/resources/lambdabodystaticunsolved2/expected/com/example/nullness/Nullness.java b/src/test/resources/lambdabodystaticunsolved2/expected/com/example/nullness/Nullness.java index eab08402a..f50a8efc3 100644 --- a/src/test/resources/lambdabodystaticunsolved2/expected/com/example/nullness/Nullness.java +++ b/src/test/resources/lambdabodystaticunsolved2/expected/com/example/nullness/Nullness.java @@ -2,7 +2,7 @@ public class Nullness { - public static SyntheticUnconstrainedType castNonNull(java.lang.Object parameter0) { + public static com.example.sql.SqlParserPos castNonNull(java.lang.Object parameter0) { throw new java.lang.Error(); } } \ No newline at end of file diff --git a/src/test/resources/lambdaconsumer/expected/com/example/SyntheticTypeForX.java b/src/test/resources/lambdaconsumer/expected/com/example/SyntheticTypeForX.java new file mode 100644 index 000000000..f4fa6b0d9 --- /dev/null +++ b/src/test/resources/lambdaconsumer/expected/com/example/SyntheticTypeForX.java @@ -0,0 +1,3 @@ +package com.example; +public class SyntheticTypeForX { +} diff --git a/src/test/resources/lambdaconsumer/expected/org/example/LambdaUser.java b/src/test/resources/lambdaconsumer/expected/org/example/LambdaUser.java index 88cfb918d..4e7f120ed 100644 --- a/src/test/resources/lambdaconsumer/expected/org/example/LambdaUser.java +++ b/src/test/resources/lambdaconsumer/expected/org/example/LambdaUser.java @@ -1,7 +1,7 @@ package org.example; public class LambdaUser { - public UseReturnType use(java.util.function.Consumer parameter0) { + public org.example.UseReturnType use(java.util.function.Consumer parameter0) { throw new java.lang.Error(); } } \ No newline at end of file diff --git a/src/test/resources/lambdafunction/expected/org/example/LambdaUser.java b/src/test/resources/lambdafunction/expected/org/example/LambdaUser.java index e4b725194..199a0899f 100644 --- a/src/test/resources/lambdafunction/expected/org/example/LambdaUser.java +++ b/src/test/resources/lambdafunction/expected/org/example/LambdaUser.java @@ -2,7 +2,7 @@ public class LambdaUser { - public UseReturnType use(java.util.function.Function parameter0) { + public org.example.UseReturnType use(java.util.function.Function parameter0) { throw new java.lang.Error(); } } \ No newline at end of file diff --git a/src/test/resources/lambdaparamwithtype/expected/org/example/FunctionalProgramming.java b/src/test/resources/lambdaparamwithtype/expected/org/example/FunctionalProgramming.java index f0cb19c83..3b8b1bdd8 100644 --- a/src/test/resources/lambdaparamwithtype/expected/org/example/FunctionalProgramming.java +++ b/src/test/resources/lambdaparamwithtype/expected/org/example/FunctionalProgramming.java @@ -1,7 +1,7 @@ package org.example; public class FunctionalProgramming { - public static OrgExampleFunctionalProgrammingMapReturnType map(java.util.function.Function parameter0) { + public static org.example.OrgExampleFunctionalProgrammingMapReturnType map(java.util.function.Function parameter0) { throw new java.lang.Error(); } } \ No newline at end of file diff --git a/src/test/resources/lambdaparamwithtypevar/expected/com/example/Simple.java b/src/test/resources/lambdaparamwithtypevar/expected/com/example/Simple.java index f8ae3b54c..838edf3e2 100644 --- a/src/test/resources/lambdaparamwithtypevar/expected/com/example/Simple.java +++ b/src/test/resources/lambdaparamwithtypevar/expected/com/example/Simple.java @@ -1,8 +1,8 @@ package com.example; +import com.example.myotherpkg.MyOtherObject; import org.example.FunctionalProgramming; import org.example.MyList; -import com.example.myotherpkg.MyOtherObject; class Simple { diff --git a/src/test/resources/lambdaparamwithtypevar/expected/org/example/FunctionalProgramming.java b/src/test/resources/lambdaparamwithtypevar/expected/org/example/FunctionalProgramming.java index f0cb19c83..3b8b1bdd8 100644 --- a/src/test/resources/lambdaparamwithtypevar/expected/org/example/FunctionalProgramming.java +++ b/src/test/resources/lambdaparamwithtypevar/expected/org/example/FunctionalProgramming.java @@ -1,7 +1,7 @@ package org.example; public class FunctionalProgramming { - public static OrgExampleFunctionalProgrammingMapReturnType map(java.util.function.Function parameter0) { + public static org.example.OrgExampleFunctionalProgrammingMapReturnType map(java.util.function.Function parameter0) { throw new java.lang.Error(); } } \ No newline at end of file diff --git a/src/test/resources/lambdaparamwithtypevar/expected/org/example/MyList.java b/src/test/resources/lambdaparamwithtypevar/expected/org/example/MyList.java index b7d8d0b61..fc3b237e0 100644 --- a/src/test/resources/lambdaparamwithtypevar/expected/org/example/MyList.java +++ b/src/test/resources/lambdaparamwithtypevar/expected/org/example/MyList.java @@ -1,7 +1,7 @@ package org.example; public class MyList { - public ToArrayReturnType toArray() { + public org.example.ToArrayReturnType toArray() { throw new java.lang.Error(); } } \ No newline at end of file diff --git a/src/test/resources/lambdaquadfunction/expected/org/example/SyntheticFunction4.java b/src/test/resources/lambdaquadfunction/expected/com/example/SyntheticFunction4.java similarity index 76% rename from src/test/resources/lambdaquadfunction/expected/org/example/SyntheticFunction4.java rename to src/test/resources/lambdaquadfunction/expected/com/example/SyntheticFunction4.java index 981b66402..fc0dde89b 100644 --- a/src/test/resources/lambdaquadfunction/expected/org/example/SyntheticFunction4.java +++ b/src/test/resources/lambdaquadfunction/expected/com/example/SyntheticFunction4.java @@ -1,5 +1,6 @@ -package org.example; +package com.example; +@FunctionalInterface public interface SyntheticFunction4 { public T4 apply(T parameter0, T1 parameter1, T2 parameter2, T3 parameter3); diff --git a/src/test/resources/lambdaquadfunction/expected/org/example/LambdaUser.java b/src/test/resources/lambdaquadfunction/expected/org/example/LambdaUser.java index 015eeb743..55c42fc26 100644 --- a/src/test/resources/lambdaquadfunction/expected/org/example/LambdaUser.java +++ b/src/test/resources/lambdaquadfunction/expected/org/example/LambdaUser.java @@ -1,7 +1,7 @@ package org.example; public class LambdaUser { - public UseReturnType use(SyntheticFunction4 parameter0) { + public org.example.UseReturnType use(com.example.SyntheticFunction4 parameter0) { throw new java.lang.Error(); } } \ No newline at end of file diff --git a/src/test/resources/lambdasupplier/expected/org/example/LambdaUser.java b/src/test/resources/lambdasupplier/expected/org/example/LambdaUser.java index b03c23c72..6ea8cc9b4 100644 --- a/src/test/resources/lambdasupplier/expected/org/example/LambdaUser.java +++ b/src/test/resources/lambdasupplier/expected/org/example/LambdaUser.java @@ -1,7 +1,7 @@ package org.example; public class LambdaUser { - public UseReturnType use(java.util.function.Supplier parameter0) { + public org.example.UseReturnType use(java.util.function.Supplier parameter0) { throw new java.lang.Error(); } } \ No newline at end of file diff --git a/src/test/resources/lambdatriconsumer/expected/org/example/SyntheticConsumer3.java b/src/test/resources/lambdatriconsumer/expected/com/example/SyntheticConsumer3.java similarity index 73% rename from src/test/resources/lambdatriconsumer/expected/org/example/SyntheticConsumer3.java rename to src/test/resources/lambdatriconsumer/expected/com/example/SyntheticConsumer3.java index 389160345..970c1492f 100644 --- a/src/test/resources/lambdatriconsumer/expected/org/example/SyntheticConsumer3.java +++ b/src/test/resources/lambdatriconsumer/expected/com/example/SyntheticConsumer3.java @@ -1,5 +1,6 @@ -package org.example; +package com.example; +@FunctionalInterface public interface SyntheticConsumer3 { public void apply(T parameter0, T1 parameter1, T2 parameter2); diff --git a/src/test/resources/lambdatriconsumer/expected/org/example/LambdaUser.java b/src/test/resources/lambdatriconsumer/expected/org/example/LambdaUser.java index d9db2ce3e..7d4a3cb3b 100644 --- a/src/test/resources/lambdatriconsumer/expected/org/example/LambdaUser.java +++ b/src/test/resources/lambdatriconsumer/expected/org/example/LambdaUser.java @@ -1,7 +1,7 @@ package org.example; public class LambdaUser { - public UseReturnType use(SyntheticConsumer3 parameter0) { + public org.example.UseReturnType use(com.example.SyntheticConsumer3 parameter0) { throw new java.lang.Error(); } } \ No newline at end of file diff --git a/src/test/resources/lambdatrifunction/expected/org/example/SyntheticFunction3.java b/src/test/resources/lambdatrifunction/expected/com/example/SyntheticFunction3.java similarity index 74% rename from src/test/resources/lambdatrifunction/expected/org/example/SyntheticFunction3.java rename to src/test/resources/lambdatrifunction/expected/com/example/SyntheticFunction3.java index 2111bd378..9de70d691 100644 --- a/src/test/resources/lambdatrifunction/expected/org/example/SyntheticFunction3.java +++ b/src/test/resources/lambdatrifunction/expected/com/example/SyntheticFunction3.java @@ -1,5 +1,6 @@ -package org.example; +package com.example; +@FunctionalInterface public interface SyntheticFunction3 { public T3 apply(T parameter0, T1 parameter1, T2 parameter2); diff --git a/src/test/resources/lambdatrifunction/expected/org/example/LambdaUser.java b/src/test/resources/lambdatrifunction/expected/org/example/LambdaUser.java index 50c3b1563..775ba2d80 100644 --- a/src/test/resources/lambdatrifunction/expected/org/example/LambdaUser.java +++ b/src/test/resources/lambdatrifunction/expected/org/example/LambdaUser.java @@ -1,7 +1,7 @@ package org.example; public class LambdaUser { - public UseReturnType use(SyntheticFunction3 parameter0) { + public org.example.UseReturnType use(com.example.SyntheticFunction3 parameter0) { throw new java.lang.Error(); } } \ No newline at end of file diff --git a/src/test/resources/lambdatrifunction2/expected/org/example/SyntheticFunction3.java b/src/test/resources/lambdatrifunction2/expected/com/example/SyntheticFunction3.java similarity index 74% rename from src/test/resources/lambdatrifunction2/expected/org/example/SyntheticFunction3.java rename to src/test/resources/lambdatrifunction2/expected/com/example/SyntheticFunction3.java index 2111bd378..9de70d691 100644 --- a/src/test/resources/lambdatrifunction2/expected/org/example/SyntheticFunction3.java +++ b/src/test/resources/lambdatrifunction2/expected/com/example/SyntheticFunction3.java @@ -1,5 +1,6 @@ -package org.example; +package com.example; +@FunctionalInterface public interface SyntheticFunction3 { public T3 apply(T parameter0, T1 parameter1, T2 parameter2); diff --git a/src/test/resources/lambdatrifunction2/expected/org/example/LambdaUser.java b/src/test/resources/lambdatrifunction2/expected/org/example/LambdaUser.java index 50c3b1563..775ba2d80 100644 --- a/src/test/resources/lambdatrifunction2/expected/org/example/LambdaUser.java +++ b/src/test/resources/lambdatrifunction2/expected/org/example/LambdaUser.java @@ -1,7 +1,7 @@ package org.example; public class LambdaUser { - public UseReturnType use(SyntheticFunction3 parameter0) { + public org.example.UseReturnType use(com.example.SyntheticFunction3 parameter0) { throw new java.lang.Error(); } } \ No newline at end of file diff --git a/src/test/resources/localvarinexception/expected/javalanguage/Method.java b/src/test/resources/localvarinexception/expected/javalanguage/Method.java index 8d7406be1..400d1dece 100644 --- a/src/test/resources/localvarinexception/expected/javalanguage/Method.java +++ b/src/test/resources/localvarinexception/expected/javalanguage/Method.java @@ -2,11 +2,11 @@ public class Method { - public Method() { + public javalanguage.SolveReturnType solve() { throw new java.lang.Error(); } - public SolveReturnType solve() { + public Method() { throw new java.lang.Error(); } } diff --git a/src/test/resources/mapofunsolved/expected/com/example/Simple.java b/src/test/resources/mapofunsolved/expected/com/example/Simple.java index dc1bc6d42..313e4c7b1 100644 --- a/src/test/resources/mapofunsolved/expected/com/example/Simple.java +++ b/src/test/resources/mapofunsolved/expected/com/example/Simple.java @@ -1,8 +1,8 @@ package com.example; import java.util.IdentityHashMap; -import org.testing.BigTree; import java.util.Set; +import org.testing.BigTree; class Simple { diff --git a/src/test/resources/methodref2/expected/com/example/MethodSignature.java b/src/test/resources/methodref2/expected/com/example/MethodSignature.java index 69026266a..cf5a91aa2 100644 --- a/src/test/resources/methodref2/expected/com/example/MethodSignature.java +++ b/src/test/resources/methodref2/expected/com/example/MethodSignature.java @@ -1,8 +1,3 @@ package com.example; -public class MethodSignature { - - public String toString() { - throw new java.lang.Error(); - } -} \ No newline at end of file +public class MethodSignature {} \ No newline at end of file diff --git a/src/test/resources/methodref2/expected/com/example/Simple.java b/src/test/resources/methodref2/expected/com/example/Simple.java index 3cf9bdb5b..254dfd046 100644 --- a/src/test/resources/methodref2/expected/com/example/Simple.java +++ b/src/test/resources/methodref2/expected/com/example/Simple.java @@ -1,8 +1,7 @@ package com.example; -import java.util.Map; import java.util.List; - +import java.util.Map; import org.plumelib.util.CollectionsPlume; class Simple { @@ -10,6 +9,7 @@ class Simple { Map replacementMap; void bar() { - List signatureList = CollectionsPlume.mapList(MethodSignature::toString, replacementMap.keySet()); + List signatureList = + CollectionsPlume.mapList(MethodSignature::toString, replacementMap.keySet()); } } diff --git a/src/test/resources/methodref2/expected/org/plumelib/util/CollectionsPlume.java b/src/test/resources/methodref2/expected/org/plumelib/util/CollectionsPlume.java index 4bec1d6a7..13986b42c 100644 --- a/src/test/resources/methodref2/expected/org/plumelib/util/CollectionsPlume.java +++ b/src/test/resources/methodref2/expected/org/plumelib/util/CollectionsPlume.java @@ -2,7 +2,7 @@ public class CollectionsPlume { - public static OrgPlumelibUtilCollectionsPlumeMapListReturnType mapList(java.util.function.Supplier parameter0, java.util.Set parameter1) { + public static java.util.List mapList(java.util.function.Function parameter0, java.util.Set parameter1) { throw new java.lang.Error(); } } \ No newline at end of file diff --git a/src/test/resources/methodref2/expected/org/plumelib/util/OrgPlumelibUtilCollectionsPlumeMapListReturnType.java b/src/test/resources/methodref2/expected/org/plumelib/util/OrgPlumelibUtilCollectionsPlumeMapListReturnType.java deleted file mode 100644 index 0e3db04f1..000000000 --- a/src/test/resources/methodref2/expected/org/plumelib/util/OrgPlumelibUtilCollectionsPlumeMapListReturnType.java +++ /dev/null @@ -1,4 +0,0 @@ -package org.plumelib.util; - -public class OrgPlumelibUtilCollectionsPlumeMapListReturnType { -} \ No newline at end of file diff --git a/src/test/resources/methodreturnfullyqualifiedgeneric/expected/com/bar/Bar.java b/src/test/resources/methodreturnfullyqualifiedgeneric/expected/com/bar/Bar.java index 3cc4b68ee..389d2da71 100644 --- a/src/test/resources/methodreturnfullyqualifiedgeneric/expected/com/bar/Bar.java +++ b/src/test/resources/methodreturnfullyqualifiedgeneric/expected/com/bar/Bar.java @@ -2,7 +2,7 @@ public class Bar { - public static com.bar.Bar getOtherPackage2() { + public static com.bar.Bar getOtherPackage2() { throw new java.lang.Error(); } } diff --git a/src/test/resources/methodreturngeneric/expected/com/bar/Bar.java b/src/test/resources/methodreturngeneric/expected/com/bar/Bar.java index 893454743..c3ad66c36 100644 --- a/src/test/resources/methodreturngeneric/expected/com/bar/Bar.java +++ b/src/test/resources/methodreturngeneric/expected/com/bar/Bar.java @@ -1,8 +1,8 @@ package com.bar; public class Bar { - - public static com.bar.Bar getOtherPackage() { + + public static com.bar.Bar getJavaLang() { throw new java.lang.Error(); } @@ -10,7 +10,7 @@ public static com.bar.Bar getSamePackage() { throw new java.lang.Error(); } - public static com.bar.Bar getJavaLang() { + public static com.bar.Bar getOtherPackage() { throw new java.lang.Error(); } } diff --git a/src/test/resources/multipleboundtypeparameter/expected/org/testing/UnsolvedType.java b/src/test/resources/multipleboundtypeparameter/expected/org/testing/UnsolvedType.java index cec1d9fa6..cbfe27064 100644 --- a/src/test/resources/multipleboundtypeparameter/expected/org/testing/UnsolvedType.java +++ b/src/test/resources/multipleboundtypeparameter/expected/org/testing/UnsolvedType.java @@ -2,7 +2,7 @@ public class UnsolvedType { - public PrintReturnType print() { + public org.testing.PrintReturnType print() { throw new java.lang.Error(); } } diff --git a/src/test/resources/mustimplementmethodscomplex/expected/com/example/Foo.java b/src/test/resources/mustimplementmethodscomplex/expected/com/example/Foo.java new file mode 100644 index 000000000..0687387d4 --- /dev/null +++ b/src/test/resources/mustimplementmethodscomplex/expected/com/example/Foo.java @@ -0,0 +1,11 @@ +package com.example; + +public class Foo implements MyComparable { + public int compareTo(String o) { + throw new java.lang.Error(); + } + + public void foo() { + throw new java.lang.Error(); + } +} diff --git a/src/test/resources/mustimplementmethodscomplex/expected/com/example/MyComparable.java b/src/test/resources/mustimplementmethodscomplex/expected/com/example/MyComparable.java new file mode 100644 index 000000000..77830e990 --- /dev/null +++ b/src/test/resources/mustimplementmethodscomplex/expected/com/example/MyComparable.java @@ -0,0 +1,3 @@ +package com.example; + +public interface MyComparable extends Comparable {} diff --git a/src/test/resources/mustimplementmethodscomplex/input/com/example/Foo.java b/src/test/resources/mustimplementmethodscomplex/input/com/example/Foo.java new file mode 100644 index 000000000..f834b8142 --- /dev/null +++ b/src/test/resources/mustimplementmethodscomplex/input/com/example/Foo.java @@ -0,0 +1,14 @@ +package com.example; + +// Note that the type variable map is String --> T --> E +public class Foo implements MyComparable { + @Override + public int compareTo(String o) { + throw new java.lang.Error(); + } + + // Target + public void foo() { + throw new java.lang.Error(); + } +} diff --git a/src/test/resources/mustimplementmethodscomplex/input/com/example/MyComparable.java b/src/test/resources/mustimplementmethodscomplex/input/com/example/MyComparable.java new file mode 100644 index 000000000..093fd6483 --- /dev/null +++ b/src/test/resources/mustimplementmethodscomplex/input/com/example/MyComparable.java @@ -0,0 +1,5 @@ +package com.example; + +public interface MyComparable extends Comparable { + +} diff --git a/src/test/resources/navigablesetexample2/expected/com/example/NavigableSet.java b/src/test/resources/navigablesetexample2/expected/com/example/NavigableSet.java index 948c59ad6..4d4d26cd9 100644 --- a/src/test/resources/navigablesetexample2/expected/com/example/NavigableSet.java +++ b/src/test/resources/navigablesetexample2/expected/com/example/NavigableSet.java @@ -1,4 +1,4 @@ package com.example; -public interface NavigableSet { +public interface NavigableSet { } diff --git a/src/test/resources/navigablesetfieldexample/expected/com/example/Collection.java b/src/test/resources/navigablesetfieldexample/expected/com/example/Collection.java new file mode 100644 index 000000000..004b4a4f4 --- /dev/null +++ b/src/test/resources/navigablesetfieldexample/expected/com/example/Collection.java @@ -0,0 +1,3 @@ +package com.example; +public interface Collection { +} diff --git a/src/test/resources/navigablesetfieldexample/expected/com/example/NavigableSet.java b/src/test/resources/navigablesetfieldexample/expected/com/example/NavigableSet.java index 4d4d26cd9..47a15eafb 100644 --- a/src/test/resources/navigablesetfieldexample/expected/com/example/NavigableSet.java +++ b/src/test/resources/navigablesetfieldexample/expected/com/example/NavigableSet.java @@ -1,4 +1,4 @@ package com.example; -public interface NavigableSet { +public interface NavigableSet extends com.example.SortedSet { } diff --git a/src/test/resources/navigablesetfieldexample/expected/com/example/Set.java b/src/test/resources/navigablesetfieldexample/expected/com/example/Set.java new file mode 100644 index 000000000..bbcd7987c --- /dev/null +++ b/src/test/resources/navigablesetfieldexample/expected/com/example/Set.java @@ -0,0 +1,3 @@ +package com.example; +public interface Set extends com.example.Collection { +} diff --git a/src/test/resources/navigablesetfieldexample/expected/com/example/Simple.java b/src/test/resources/navigablesetfieldexample/expected/com/example/Simple.java index dab80fbf5..901734d01 100644 --- a/src/test/resources/navigablesetfieldexample/expected/com/example/Simple.java +++ b/src/test/resources/navigablesetfieldexample/expected/com/example/Simple.java @@ -1,26 +1,46 @@ package com.example; +import java.io.Serializable; + public class Simple { - static class UnmodifiableCollection { + static class UnmodifiableCollection { + + UnmodifiableCollection(Collection c) { + throw new java.lang.Error(); } + } + + static class UnmodifiableSet extends UnmodifiableCollection { - static class UnmodifiableSet extends UnmodifiableCollection { + UnmodifiableSet(Set s1) { + super(s1); } + } - static class UnmodifiableSortedSet extends UnmodifiableSet { + static class UnmodifiableSortedSet extends UnmodifiableSet { + + UnmodifiableSortedSet(SortedSet s) { + super(s); } + } - static class UnmodifiableNavigableSet extends UnmodifiableSortedSet implements NavigableSet { + static class UnmodifiableNavigableSet extends UnmodifiableSortedSet + implements NavigableSet { - private static class EmptyNavigableSet extends UnmodifiableNavigableSet { + UnmodifiableNavigableSet(NavigableSet s) { + super(s); + } - public EmptyNavigableSet() { - throw new java.lang.Error(); - } - } + private static class EmptyNavigableSet extends UnmodifiableNavigableSet + implements Serializable { - @SuppressWarnings("rawtypes") - private static final NavigableSet EMPTY_NAVIGABLE_SET = new EmptyNavigableSet<>(); + public EmptyNavigableSet() { + super(new TreeSet()); + } } + + @SuppressWarnings("rawtypes") + private static final NavigableSet EMPTY_NAVIGABLE_SET = new EmptyNavigableSet<>(); + } } diff --git a/src/test/resources/navigablesetfieldexample/expected/com/example/SortedSet.java b/src/test/resources/navigablesetfieldexample/expected/com/example/SortedSet.java new file mode 100644 index 000000000..f34013335 --- /dev/null +++ b/src/test/resources/navigablesetfieldexample/expected/com/example/SortedSet.java @@ -0,0 +1,3 @@ +package com.example; +public interface SortedSet extends com.example.Set { +} diff --git a/src/test/resources/navigablesetfieldexample/expected/com/example/TreeSet.java b/src/test/resources/navigablesetfieldexample/expected/com/example/TreeSet.java new file mode 100644 index 000000000..6239c29a1 --- /dev/null +++ b/src/test/resources/navigablesetfieldexample/expected/com/example/TreeSet.java @@ -0,0 +1,7 @@ +package com.example; +public class TreeSet implements com.example.NavigableSet { + + public TreeSet() { + throw new java.lang.Error(); + } +} diff --git a/src/test/resources/navigablesetfieldexample/input/com/example/Simple.java b/src/test/resources/navigablesetfieldexample/input/com/example/Simple.java index d1a24ee80..e810dee65 100644 --- a/src/test/resources/navigablesetfieldexample/input/com/example/Simple.java +++ b/src/test/resources/navigablesetfieldexample/input/com/example/Simple.java @@ -7,6 +7,7 @@ public class Simple { + // TODO: only preserve EmptyNavigableSet#EmptyNavigableSet() and none of the superclass constructors. static class UnmodifiableCollection { UnmodifiableCollection(Collection c) { } diff --git a/src/test/resources/nestedcatchclause/expected/com/example/CustomException.java b/src/test/resources/nestedcatchclause/expected/com/example/CustomException.java index 057fb5766..d35372bca 100644 --- a/src/test/resources/nestedcatchclause/expected/com/example/CustomException.java +++ b/src/test/resources/nestedcatchclause/expected/com/example/CustomException.java @@ -1,6 +1,6 @@ package com.example; -public class CustomException extends Exception { +public class CustomException extends java.lang.Exception { public CustomException(String msg) { throw new java.lang.Error(); diff --git a/src/test/resources/nestedcatchclause/input/com/example/CustomException.java b/src/test/resources/nestedcatchclause/input/com/example/CustomException.java index a0fcf0e3f..8111dc268 100644 --- a/src/test/resources/nestedcatchclause/input/com/example/CustomException.java +++ b/src/test/resources/nestedcatchclause/input/com/example/CustomException.java @@ -1,5 +1,5 @@ package com.example; -public class CustomException extends Exception { +public class CustomException extends java.lang.Exception { public CustomException (String msg) { } } diff --git a/src/test/resources/nullarg/expected/org/example/Baz.java b/src/test/resources/nullarg/expected/org/example/Baz.java index 05f6dcb3c..d4c74db76 100644 --- a/src/test/resources/nullarg/expected/org/example/Baz.java +++ b/src/test/resources/nullarg/expected/org/example/Baz.java @@ -2,7 +2,7 @@ public class Baz { - public QuxReturnType qux(java.lang.Object parameter0) { + public org.example.QuxReturnType qux(java.lang.Object parameter0) { throw new java.lang.Error(); } } \ No newline at end of file diff --git a/src/test/resources/overloads/expected/org/example/MultipleMethods.java b/src/test/resources/overloads/expected/org/example/MultipleMethods.java index 4573274d8..e66f18ef4 100644 --- a/src/test/resources/overloads/expected/org/example/MultipleMethods.java +++ b/src/test/resources/overloads/expected/org/example/MultipleMethods.java @@ -1,31 +1,31 @@ package org.example; public class MultipleMethods { - public ExampleReturnType example() { + public org.example.ExampleReturnType example(java.lang.String parameter0, java.lang.String parameter1) { throw new java.lang.Error(); } - public ExampleReturnType example(java.lang.String parameter0) { + public org.example.ExampleReturnType example(int parameter0, int parameter1) { throw new java.lang.Error(); } - public ExampleReturnType example(int parameter0) { + public org.example.ExampleReturnType example(int parameter0, java.lang.String parameter1) { throw new java.lang.Error(); } - public ExampleReturnType example(java.lang.String parameter0, int parameter1) { + public org.example.ExampleReturnType example(java.lang.String parameter0, int parameter1) { throw new java.lang.Error(); } - public ExampleReturnType example(int parameter0, java.lang.String parameter1) { + public org.example.ExampleReturnType example(int parameter0) { throw new java.lang.Error(); } - public ExampleReturnType example(int parameter0, int parameter1) { + public org.example.ExampleReturnType example(java.lang.String parameter0) { throw new java.lang.Error(); } - public ExampleReturnType example(java.lang.String parameter0, java.lang.String parameter1) { + public org.example.ExampleReturnType example() { throw new java.lang.Error(); } } \ No newline at end of file diff --git a/src/test/resources/parameterwithannotations/expected/org/checkerframework/checker/nullness/qual/Nullable.java b/src/test/resources/parameterwithannotations/expected/org/checkerframework/checker/nullness/qual/Nullable.java index e0b78bcde..0792cbabf 100644 --- a/src/test/resources/parameterwithannotations/expected/org/checkerframework/checker/nullness/qual/Nullable.java +++ b/src/test/resources/parameterwithannotations/expected/org/checkerframework/checker/nullness/qual/Nullable.java @@ -12,9 +12,9 @@ @Documented @Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.TYPE_USE, ElementType.TYPE_PARAMETER }) @SubtypeOf({}) @QualifierForLiterals({ LiteralKind.NULL }) @DefaultFor(types = { Void.class }) -@Target({ ElementType.TYPE_USE }) public @interface Nullable { } diff --git a/src/test/resources/parameterwithannotations/expected/org/checkerframework/framework/qual/SubtypeOf.java b/src/test/resources/parameterwithannotations/expected/org/checkerframework/framework/qual/SubtypeOf.java index ed6c05afc..9690bec5b 100644 --- a/src/test/resources/parameterwithannotations/expected/org/checkerframework/framework/qual/SubtypeOf.java +++ b/src/test/resources/parameterwithannotations/expected/org/checkerframework/framework/qual/SubtypeOf.java @@ -1,5 +1,6 @@ package org.checkerframework.framework.qual; +import java.lang.annotation.Annotation; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; @@ -11,5 +12,5 @@ @Target({ ElementType.ANNOTATION_TYPE }) public @interface SubtypeOf { - Class[] value(); + Class[] value(); } diff --git a/src/test/resources/parameterwithannotations/expected/org/checkerframework/framework/qual/TypeKind.java b/src/test/resources/parameterwithannotations/expected/org/checkerframework/framework/qual/TypeKind.java new file mode 100644 index 000000000..6368717d3 --- /dev/null +++ b/src/test/resources/parameterwithannotations/expected/org/checkerframework/framework/qual/TypeKind.java @@ -0,0 +1,4 @@ +package org.checkerframework.framework.qual; + +public enum TypeKind { +} diff --git a/src/test/resources/parameterwithannotations/expected/org/checkerframework/framework/qual/TypeUseLocation.java b/src/test/resources/parameterwithannotations/expected/org/checkerframework/framework/qual/TypeUseLocation.java new file mode 100644 index 000000000..3dd553f86 --- /dev/null +++ b/src/test/resources/parameterwithannotations/expected/org/checkerframework/framework/qual/TypeUseLocation.java @@ -0,0 +1,4 @@ +package org.checkerframework.framework.qual; + +public enum TypeUseLocation { +} diff --git a/src/test/resources/preserveannotations/expected/org/checkerframework/checker/index/qual/GTENegativeOne.java b/src/test/resources/preserveannotations/expected/org/checkerframework/checker/index/qual/GTENegativeOne.java index 9feacb455..05f431ebd 100644 --- a/src/test/resources/preserveannotations/expected/org/checkerframework/checker/index/qual/GTENegativeOne.java +++ b/src/test/resources/preserveannotations/expected/org/checkerframework/checker/index/qual/GTENegativeOne.java @@ -1,6 +1,7 @@ package org.checkerframework.checker.index.qual; import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @@ -8,7 +9,7 @@ @Documented @Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.TYPE_USE, ElementType.TYPE_PARAMETER }) @SubtypeOf({ LowerBoundUnknown.class }) -@Target({}) public @interface GTENegativeOne { } diff --git a/src/test/resources/preserveannotations/expected/org/checkerframework/checker/index/qual/LowerBoundUnknown.java b/src/test/resources/preserveannotations/expected/org/checkerframework/checker/index/qual/LowerBoundUnknown.java index 3ed496c62..0ba975cb6 100644 --- a/src/test/resources/preserveannotations/expected/org/checkerframework/checker/index/qual/LowerBoundUnknown.java +++ b/src/test/resources/preserveannotations/expected/org/checkerframework/checker/index/qual/LowerBoundUnknown.java @@ -1,6 +1,7 @@ package org.checkerframework.checker.index.qual; import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @@ -10,9 +11,9 @@ @Documented @Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.TYPE_USE, ElementType.TYPE_PARAMETER }) @SubtypeOf({}) @DefaultQualifierInHierarchy @InvisibleQualifier -@Target({}) public @interface LowerBoundUnknown { } diff --git a/src/test/resources/preserveannotations/expected/org/checkerframework/checker/index/qual/NonNegative.java b/src/test/resources/preserveannotations/expected/org/checkerframework/checker/index/qual/NonNegative.java index 60d78c603..53e2a6475 100644 --- a/src/test/resources/preserveannotations/expected/org/checkerframework/checker/index/qual/NonNegative.java +++ b/src/test/resources/preserveannotations/expected/org/checkerframework/checker/index/qual/NonNegative.java @@ -9,7 +9,7 @@ @Documented @Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.TYPE_USE, ElementType.TYPE_PARAMETER }) @SubtypeOf({ GTENegativeOne.class }) -@Target({ ElementType.TYPE_USE }) public @interface NonNegative { } diff --git a/src/test/resources/preserveannotations/expected/org/checkerframework/checker/index/qual/Positive.java b/src/test/resources/preserveannotations/expected/org/checkerframework/checker/index/qual/Positive.java index dc90a1538..e021f7dc4 100644 --- a/src/test/resources/preserveannotations/expected/org/checkerframework/checker/index/qual/Positive.java +++ b/src/test/resources/preserveannotations/expected/org/checkerframework/checker/index/qual/Positive.java @@ -9,7 +9,7 @@ @Documented @Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.TYPE_USE, ElementType.TYPE_PARAMETER }) @SubtypeOf({ NonNegative.class }) -@Target({ ElementType.TYPE_USE }) public @interface Positive { } diff --git a/src/test/resources/preserveannotations/expected/org/checkerframework/framework/qual/SubtypeOf.java b/src/test/resources/preserveannotations/expected/org/checkerframework/framework/qual/SubtypeOf.java index ed6c05afc..9690bec5b 100644 --- a/src/test/resources/preserveannotations/expected/org/checkerframework/framework/qual/SubtypeOf.java +++ b/src/test/resources/preserveannotations/expected/org/checkerframework/framework/qual/SubtypeOf.java @@ -1,5 +1,6 @@ package org.checkerframework.framework.qual; +import java.lang.annotation.Annotation; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; @@ -11,5 +12,5 @@ @Target({ ElementType.ANNOTATION_TYPE }) public @interface SubtypeOf { - Class[] value(); + Class[] value(); } diff --git a/src/test/resources/privateinnerclass/expected/com/example/MyList.java b/src/test/resources/privateinnerclass/expected/com/example/MyList.java index d8a53fdd9..2410f1052 100644 --- a/src/test/resources/privateinnerclass/expected/com/example/MyList.java +++ b/src/test/resources/privateinnerclass/expected/com/example/MyList.java @@ -2,11 +2,11 @@ public class MyList { - public MyList() { + public com.example.AddReturnType add(java.lang.Object parameter0) { throw new java.lang.Error(); } - public AddReturnType add(java.lang.Object parameter0) { + public MyList() { throw new java.lang.Error(); } } diff --git a/src/test/resources/realsuperlub/expected/com/example/SyntheticTypeForChild.java b/src/test/resources/realsuperlub/expected/com/example/SyntheticTypeForChild.java deleted file mode 100644 index 9ae7f64ff..000000000 --- a/src/test/resources/realsuperlub/expected/com/example/SyntheticTypeForChild.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.example; - -public class SyntheticTypeForChild { - -} \ No newline at end of file diff --git a/src/test/resources/realsuperlub/expected/org/mygraphlib/Node.java b/src/test/resources/realsuperlub/expected/org/mygraphlib/Node.java index 55c312d66..205dc0517 100644 --- a/src/test/resources/realsuperlub/expected/org/mygraphlib/Node.java +++ b/src/test/resources/realsuperlub/expected/org/mygraphlib/Node.java @@ -1,6 +1,6 @@ package org.mygraphlib; -public class Node extends com.example.SyntheticTypeForChild { +public class Node { public org.mygraphlib.Node child; diff --git a/src/test/resources/staticandnonstaticuse/expected/com/example/Simple.java b/src/test/resources/staticandnonstaticuse/expected/com/example/Simple.java index c155fd23c..9ec147d83 100644 --- a/src/test/resources/staticandnonstaticuse/expected/com/example/Simple.java +++ b/src/test/resources/staticandnonstaticuse/expected/com/example/Simple.java @@ -1,7 +1,7 @@ package com.example; -import org.example.Foo; import org.example.Bar; +import org.example.Foo; public class Simple { diff --git a/src/test/resources/staticandnonstaticuse/expected/org/example/Foo.java b/src/test/resources/staticandnonstaticuse/expected/org/example/Foo.java index 44d748faf..4e43fe542 100644 --- a/src/test/resources/staticandnonstaticuse/expected/org/example/Foo.java +++ b/src/test/resources/staticandnonstaticuse/expected/org/example/Foo.java @@ -2,11 +2,11 @@ public class Foo { - public Foo() { + public static org.example.OrgExampleFooSetThisReturnType setThis(org.example.Bar parameter0) { throw new java.lang.Error(); } - public static OrgExampleFooSetThisReturnType setThis(org.example.Bar parameter0) { + public Foo() { throw new java.lang.Error(); } } \ No newline at end of file diff --git a/src/test/resources/staticblock/expected/com/example/Simple.java b/src/test/resources/staticblock/expected/com/example/Simple.java index ed28ab352..f3ad44a51 100644 --- a/src/test/resources/staticblock/expected/com/example/Simple.java +++ b/src/test/resources/staticblock/expected/com/example/Simple.java @@ -2,6 +2,5 @@ class Simple { - void test() { - } + void test() {} } diff --git a/src/test/resources/staticmethod/expected/com/example/Foo.java b/src/test/resources/staticmethod/expected/com/example/Foo.java index d05786c10..903f5dcea 100644 --- a/src/test/resources/staticmethod/expected/com/example/Foo.java +++ b/src/test/resources/staticmethod/expected/com/example/Foo.java @@ -2,7 +2,7 @@ public class Foo { - public static Foo[] getFoos() { + public static com.example.Foo[] getFoos() { throw new java.lang.Error(); } } \ No newline at end of file diff --git a/src/test/resources/staticuseoferased/expected/com/example/Baz.java b/src/test/resources/staticuseoferased/expected/com/example/Baz.java index 85d4571cd..8a338516a 100644 --- a/src/test/resources/staticuseoferased/expected/com/example/Baz.java +++ b/src/test/resources/staticuseoferased/expected/com/example/Baz.java @@ -2,7 +2,7 @@ public class Baz { - public static ComExampleBazTestReturnType test() { + public static com.example.ComExampleBazTestReturnType test() { throw new java.lang.Error(); } } \ No newline at end of file diff --git a/src/test/resources/supermethodlub/expected/com/example/FooChild1.java b/src/test/resources/supermethodlub/expected/com/example/FooChild1.java new file mode 100644 index 000000000..2f6a0fa57 --- /dev/null +++ b/src/test/resources/supermethodlub/expected/com/example/FooChild1.java @@ -0,0 +1,13 @@ +package com.example; + +import org.example.Foo; + +public class FooChild1 extends Foo { + public Integer getNum() { + throw new java.lang.Error(); + } + + public Unsolved1 getUnsolved() { + throw new java.lang.Error(); + } +} diff --git a/src/test/resources/supermethodlub/expected/com/example/FooChild2.java b/src/test/resources/supermethodlub/expected/com/example/FooChild2.java new file mode 100644 index 000000000..3a6c9e1e0 --- /dev/null +++ b/src/test/resources/supermethodlub/expected/com/example/FooChild2.java @@ -0,0 +1,13 @@ +package com.example; + +import org.example.Foo; + +public class FooChild2 extends Foo { + public Long getNum() { + throw new java.lang.Error(); + } + + public Unsolved2 getUnsolved() { + throw new java.lang.Error(); + } +} diff --git a/src/test/resources/supermethodlub/expected/com/example/Simple.java b/src/test/resources/supermethodlub/expected/com/example/Simple.java new file mode 100644 index 000000000..1011608f6 --- /dev/null +++ b/src/test/resources/supermethodlub/expected/com/example/Simple.java @@ -0,0 +1,13 @@ +package com.example; + +public class Simple { + public void foo() { + FooChild1 foo1 = new FooChild1(); + foo1.getNum(); + foo1.getUnsolved(); + + FooChild2 foo2 = new FooChild2(); + foo2.getNum(); + foo2.getUnsolved(); + } +} diff --git a/src/test/resources/supermethodlub/expected/com/example/Unsolved1.java b/src/test/resources/supermethodlub/expected/com/example/Unsolved1.java new file mode 100644 index 000000000..cfa0aa873 --- /dev/null +++ b/src/test/resources/supermethodlub/expected/com/example/Unsolved1.java @@ -0,0 +1,3 @@ +package com.example; + +public class Unsolved1 {} diff --git a/src/test/resources/supermethodlub/expected/com/example/Unsolved2.java b/src/test/resources/supermethodlub/expected/com/example/Unsolved2.java new file mode 100644 index 000000000..9efc9cd78 --- /dev/null +++ b/src/test/resources/supermethodlub/expected/com/example/Unsolved2.java @@ -0,0 +1,3 @@ +package com.example; + +public class Unsolved2 extends com.example.Unsolved1 {} diff --git a/src/test/resources/supermethodlub/expected/org/example/Foo.java b/src/test/resources/supermethodlub/expected/org/example/Foo.java new file mode 100644 index 000000000..3967a0546 --- /dev/null +++ b/src/test/resources/supermethodlub/expected/org/example/Foo.java @@ -0,0 +1,12 @@ +package org.example; + +public class Foo { + + public com.example.Unsolved1 getUnsolved() { + throw new java.lang.Error(); + } + + public java.lang.Number getNum() { + throw new java.lang.Error(); + } +} diff --git a/src/test/resources/supermethodlub/input/com/example/FooChild1.java b/src/test/resources/supermethodlub/input/com/example/FooChild1.java new file mode 100644 index 000000000..374e0d9f7 --- /dev/null +++ b/src/test/resources/supermethodlub/input/com/example/FooChild1.java @@ -0,0 +1,15 @@ +package com.example; + +import org.example.Foo; + +public class FooChild1 extends Foo { + @Override + public Integer getNum() { + return 0; + } + + @Override + public Unsolved1 getUnsolved() { + throw new java.lang.Error(); + } +} diff --git a/src/test/resources/supermethodlub/input/com/example/FooChild2.java b/src/test/resources/supermethodlub/input/com/example/FooChild2.java new file mode 100644 index 000000000..862d6fef3 --- /dev/null +++ b/src/test/resources/supermethodlub/input/com/example/FooChild2.java @@ -0,0 +1,15 @@ +package com.example; + +import org.example.Foo; + +public class FooChild2 extends Foo { + @Override + public Long getNum() { + return 0L; + } + + @Override + public Unsolved2 getUnsolved() { + throw new java.lang.Error(); + } +} diff --git a/src/test/resources/supermethodlub/input/com/example/Simple.java b/src/test/resources/supermethodlub/input/com/example/Simple.java new file mode 100644 index 000000000..1011608f6 --- /dev/null +++ b/src/test/resources/supermethodlub/input/com/example/Simple.java @@ -0,0 +1,13 @@ +package com.example; + +public class Simple { + public void foo() { + FooChild1 foo1 = new FooChild1(); + foo1.getNum(); + foo1.getUnsolved(); + + FooChild2 foo2 = new FooChild2(); + foo2.getNum(); + foo2.getUnsolved(); + } +} diff --git a/src/test/resources/supermethodlub2/expected/com/example/Simple.java b/src/test/resources/supermethodlub2/expected/com/example/Simple.java new file mode 100644 index 000000000..25bfadb2c --- /dev/null +++ b/src/test/resources/supermethodlub2/expected/com/example/Simple.java @@ -0,0 +1,14 @@ +package com.example; + +import org.example.Foo; + +public class Simple { + public void foo() { + SomeChild child = new SomeChild(); + foo(child.method()); + } + + private void foo(Foo foo) { + throw new java.lang.Error(); + } +} diff --git a/src/test/resources/supermethodlub2/expected/com/example/SomeChild.java b/src/test/resources/supermethodlub2/expected/com/example/SomeChild.java new file mode 100644 index 000000000..5b90598ad --- /dev/null +++ b/src/test/resources/supermethodlub2/expected/com/example/SomeChild.java @@ -0,0 +1,10 @@ +package com.example; + +import org.example.Baz; + +public class SomeChild extends SomeParent { + + public Baz method() { + throw new java.lang.Error(); + } +} diff --git a/src/test/resources/supermethodlub2/expected/com/example/SomeParent.java b/src/test/resources/supermethodlub2/expected/com/example/SomeParent.java new file mode 100644 index 000000000..a4306e8ca --- /dev/null +++ b/src/test/resources/supermethodlub2/expected/com/example/SomeParent.java @@ -0,0 +1,7 @@ +package com.example; +public class SomeParent { + + public org.example.Baz method() { + throw new java.lang.Error(); + } +} diff --git a/src/test/resources/supermethodlub2/expected/org/example/Baz.java b/src/test/resources/supermethodlub2/expected/org/example/Baz.java new file mode 100644 index 000000000..3cc70988c --- /dev/null +++ b/src/test/resources/supermethodlub2/expected/org/example/Baz.java @@ -0,0 +1,3 @@ +package org.example; +public class Baz extends org.example.Foo { +} diff --git a/src/test/resources/supermethodlub2/expected/org/example/Foo.java b/src/test/resources/supermethodlub2/expected/org/example/Foo.java new file mode 100644 index 000000000..225111c92 --- /dev/null +++ b/src/test/resources/supermethodlub2/expected/org/example/Foo.java @@ -0,0 +1,3 @@ +package org.example; +public class Foo { +} diff --git a/src/test/resources/supermethodlub2/input/com/example/Simple.java b/src/test/resources/supermethodlub2/input/com/example/Simple.java new file mode 100644 index 000000000..1a45cacab --- /dev/null +++ b/src/test/resources/supermethodlub2/input/com/example/Simple.java @@ -0,0 +1,19 @@ +package com.example; + +import org.example.Foo; +import org.example.Bar; + +public class Simple { + public void foo() { + SomeChild child = new SomeChild(); + foo(child.method()); + } + + private void foo(Foo foo) { + + } + + private void foo(Bar bar) { + + } +} diff --git a/src/test/resources/supermethodlub2/input/com/example/SomeChild.java b/src/test/resources/supermethodlub2/input/com/example/SomeChild.java new file mode 100644 index 000000000..bd11709cd --- /dev/null +++ b/src/test/resources/supermethodlub2/input/com/example/SomeChild.java @@ -0,0 +1,10 @@ +package com.example; + +import org.example.Baz; + +public class SomeChild extends SomeParent { + @Override + public Baz method() { + throw new java.lang.Error(); + } +} diff --git a/src/test/resources/supermethodwithunsolvedparameters/expected/com/example/Simple.java b/src/test/resources/supermethodwithunsolvedparameters/expected/com/example/Simple.java index 7c9e24755..cbd30132a 100644 --- a/src/test/resources/supermethodwithunsolvedparameters/expected/com/example/Simple.java +++ b/src/test/resources/supermethodwithunsolvedparameters/expected/com/example/Simple.java @@ -1,7 +1,7 @@ package com.example; -import org.simple.SuperSimple; import org.example.Unsolved; +import org.simple.SuperSimple; class Simple extends SuperSimple { diff --git a/src/test/resources/supermethodwithunsolvedparameters/expected/org/simple/SuperSimple.java b/src/test/resources/supermethodwithunsolvedparameters/expected/org/simple/SuperSimple.java index 8a363900e..227e4993f 100644 --- a/src/test/resources/supermethodwithunsolvedparameters/expected/org/simple/SuperSimple.java +++ b/src/test/resources/supermethodwithunsolvedparameters/expected/org/simple/SuperSimple.java @@ -2,7 +2,7 @@ public class SuperSimple { - public FooReturnType foo(org.example.Unsolved parameter0) { + public org.simple.FooReturnType foo(org.example.Unsolved parameter0) { throw new java.lang.Error(); } } diff --git a/src/test/resources/syntheticannotations/expected/com/example/Anno.java b/src/test/resources/syntheticannotations/expected/com/example/Anno.java index 880aad8da..157820d2f 100644 --- a/src/test/resources/syntheticannotations/expected/com/example/Anno.java +++ b/src/test/resources/syntheticannotations/expected/com/example/Anno.java @@ -1,6 +1,6 @@ package com.example; -@java.lang.annotation.Target({ java.lang.annotation.ElementType.TYPE_USE }) +@java.lang.annotation.Target({ java.lang.annotation.ElementType.TYPE_USE, java.lang.annotation.ElementType.TYPE, java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.PARAMETER, java.lang.annotation.ElementType.CONSTRUCTOR, java.lang.annotation.ElementType.LOCAL_VARIABLE, java.lang.annotation.ElementType.ANNOTATION_TYPE, java.lang.annotation.ElementType.PACKAGE, java.lang.annotation.ElementType.TYPE_PARAMETER}) public @interface Anno { public int value(); diff --git a/src/test/resources/syntheticannotations/expected/com/example/Bar.java b/src/test/resources/syntheticannotations/expected/com/example/Bar.java index e875d21d6..1e2ca9c53 100644 --- a/src/test/resources/syntheticannotations/expected/com/example/Bar.java +++ b/src/test/resources/syntheticannotations/expected/com/example/Bar.java @@ -1,6 +1,6 @@ package com.example; -@java.lang.annotation.Target({ java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.TYPE_USE }) +@java.lang.annotation.Target({ java.lang.annotation.ElementType.TYPE_USE, java.lang.annotation.ElementType.TYPE, java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.PARAMETER, java.lang.annotation.ElementType.CONSTRUCTOR, java.lang.annotation.ElementType.LOCAL_VARIABLE, java.lang.annotation.ElementType.ANNOTATION_TYPE, java.lang.annotation.ElementType.PACKAGE, java.lang.annotation.ElementType.TYPE_PARAMETER}) public @interface Bar { public int value(); diff --git a/src/test/resources/syntheticannotations/expected/com/example/Baz.java b/src/test/resources/syntheticannotations/expected/com/example/Baz.java index 0af8ae911..245369e5b 100644 --- a/src/test/resources/syntheticannotations/expected/com/example/Baz.java +++ b/src/test/resources/syntheticannotations/expected/com/example/Baz.java @@ -1,9 +1,9 @@ package com.example; -@java.lang.annotation.Target({ java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.TYPE_USE }) +@java.lang.annotation.Target({ java.lang.annotation.ElementType.TYPE_USE, java.lang.annotation.ElementType.TYPE, java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.PARAMETER, java.lang.annotation.ElementType.CONSTRUCTOR, java.lang.annotation.ElementType.LOCAL_VARIABLE, java.lang.annotation.ElementType.ANNOTATION_TYPE, java.lang.annotation.ElementType.PACKAGE, java.lang.annotation.ElementType.TYPE_PARAMETER}) public @interface Baz { - public String foo(); + public java.lang.String foo(); - public Class bar(); + public java.lang.Class bar(); } diff --git a/src/test/resources/syntheticannotations/expected/com/example/Foo.java b/src/test/resources/syntheticannotations/expected/com/example/Foo.java index 765a8e335..74b958e36 100644 --- a/src/test/resources/syntheticannotations/expected/com/example/Foo.java +++ b/src/test/resources/syntheticannotations/expected/com/example/Foo.java @@ -1,9 +1,9 @@ package com.example; -@java.lang.annotation.Target({ java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.TYPE_USE }) +@java.lang.annotation.Target({ java.lang.annotation.ElementType.TYPE_USE, java.lang.annotation.ElementType.TYPE, java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.PARAMETER, java.lang.annotation.ElementType.CONSTRUCTOR, java.lang.annotation.ElementType.LOCAL_VARIABLE, java.lang.annotation.ElementType.ANNOTATION_TYPE, java.lang.annotation.ElementType.PACKAGE, java.lang.annotation.ElementType.TYPE_PARAMETER}) public @interface Foo { - public Deprecated x(); + public java.lang.Deprecated x(); - public Anno[] y(); + public com.example.Anno[] y(); } diff --git a/src/test/resources/syntheticannotationtarget/expected/com/example/AnnotationDeclaration.java b/src/test/resources/syntheticannotationtarget/expected/com/example/AnnotationDeclaration.java index 77aef09fc..0baba63e3 100644 --- a/src/test/resources/syntheticannotationtarget/expected/com/example/AnnotationDeclaration.java +++ b/src/test/resources/syntheticannotationtarget/expected/com/example/AnnotationDeclaration.java @@ -1,5 +1,5 @@ package com.example; -@java.lang.annotation.Target({ java.lang.annotation.ElementType.ANNOTATION_TYPE, java.lang.annotation.ElementType.TYPE, java.lang.annotation.ElementType.TYPE_USE }) +@java.lang.annotation.Target({ java.lang.annotation.ElementType.TYPE_USE, java.lang.annotation.ElementType.TYPE, java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.PARAMETER, java.lang.annotation.ElementType.CONSTRUCTOR, java.lang.annotation.ElementType.LOCAL_VARIABLE, java.lang.annotation.ElementType.ANNOTATION_TYPE, java.lang.annotation.ElementType.PACKAGE, java.lang.annotation.ElementType.TYPE_PARAMETER}) public @interface AnnotationDeclaration { } diff --git a/src/test/resources/syntheticannotationtarget/expected/com/example/Constructor.java b/src/test/resources/syntheticannotationtarget/expected/com/example/Constructor.java index 859353dc5..5483accb7 100644 --- a/src/test/resources/syntheticannotationtarget/expected/com/example/Constructor.java +++ b/src/test/resources/syntheticannotationtarget/expected/com/example/Constructor.java @@ -1,5 +1,5 @@ package com.example; -@java.lang.annotation.Target({ java.lang.annotation.ElementType.CONSTRUCTOR, java.lang.annotation.ElementType.TYPE_USE }) +@java.lang.annotation.Target({ java.lang.annotation.ElementType.TYPE_USE, java.lang.annotation.ElementType.TYPE, java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.PARAMETER, java.lang.annotation.ElementType.CONSTRUCTOR, java.lang.annotation.ElementType.LOCAL_VARIABLE, java.lang.annotation.ElementType.ANNOTATION_TYPE, java.lang.annotation.ElementType.PACKAGE, java.lang.annotation.ElementType.TYPE_PARAMETER}) public @interface Constructor { } diff --git a/src/test/resources/syntheticannotationtarget/expected/com/example/EnumConstantDeclaration.java b/src/test/resources/syntheticannotationtarget/expected/com/example/EnumConstantDeclaration.java index 48c3d4c65..db5fe0d4a 100644 --- a/src/test/resources/syntheticannotationtarget/expected/com/example/EnumConstantDeclaration.java +++ b/src/test/resources/syntheticannotationtarget/expected/com/example/EnumConstantDeclaration.java @@ -1,5 +1,5 @@ package com.example; -@java.lang.annotation.Target({ java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.TYPE_USE }) +@java.lang.annotation.Target({ java.lang.annotation.ElementType.TYPE_USE, java.lang.annotation.ElementType.TYPE, java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.PARAMETER, java.lang.annotation.ElementType.CONSTRUCTOR, java.lang.annotation.ElementType.LOCAL_VARIABLE, java.lang.annotation.ElementType.ANNOTATION_TYPE, java.lang.annotation.ElementType.PACKAGE, java.lang.annotation.ElementType.TYPE_PARAMETER}) public @interface EnumConstantDeclaration { } diff --git a/src/test/resources/syntheticannotationtarget/expected/com/example/Field.java b/src/test/resources/syntheticannotationtarget/expected/com/example/Field.java index fb44e3520..805bd04a7 100644 --- a/src/test/resources/syntheticannotationtarget/expected/com/example/Field.java +++ b/src/test/resources/syntheticannotationtarget/expected/com/example/Field.java @@ -1,5 +1,5 @@ package com.example; -@java.lang.annotation.Target({ java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.TYPE_USE }) +@java.lang.annotation.Target({ java.lang.annotation.ElementType.TYPE_USE, java.lang.annotation.ElementType.TYPE, java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.PARAMETER, java.lang.annotation.ElementType.CONSTRUCTOR, java.lang.annotation.ElementType.LOCAL_VARIABLE, java.lang.annotation.ElementType.ANNOTATION_TYPE, java.lang.annotation.ElementType.PACKAGE, java.lang.annotation.ElementType.TYPE_PARAMETER}) public @interface Field { } diff --git a/src/test/resources/syntheticannotationtarget/expected/com/example/LocalVariable.java b/src/test/resources/syntheticannotationtarget/expected/com/example/LocalVariable.java index 4788e894e..65035ec85 100644 --- a/src/test/resources/syntheticannotationtarget/expected/com/example/LocalVariable.java +++ b/src/test/resources/syntheticannotationtarget/expected/com/example/LocalVariable.java @@ -1,5 +1,5 @@ package com.example; -@java.lang.annotation.Target({ java.lang.annotation.ElementType.LOCAL_VARIABLE, java.lang.annotation.ElementType.TYPE_USE }) +@java.lang.annotation.Target({ java.lang.annotation.ElementType.TYPE_USE, java.lang.annotation.ElementType.TYPE, java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.PARAMETER, java.lang.annotation.ElementType.CONSTRUCTOR, java.lang.annotation.ElementType.LOCAL_VARIABLE, java.lang.annotation.ElementType.ANNOTATION_TYPE, java.lang.annotation.ElementType.PACKAGE, java.lang.annotation.ElementType.TYPE_PARAMETER}) public @interface LocalVariable { } diff --git a/src/test/resources/syntheticannotationtarget/expected/com/example/Method.java b/src/test/resources/syntheticannotationtarget/expected/com/example/Method.java index 625d4f55f..784528d23 100644 --- a/src/test/resources/syntheticannotationtarget/expected/com/example/Method.java +++ b/src/test/resources/syntheticannotationtarget/expected/com/example/Method.java @@ -1,5 +1,5 @@ package com.example; -@java.lang.annotation.Target({ java.lang.annotation.ElementType.METHOD }) +@java.lang.annotation.Target({ java.lang.annotation.ElementType.TYPE_USE, java.lang.annotation.ElementType.TYPE, java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.PARAMETER, java.lang.annotation.ElementType.CONSTRUCTOR, java.lang.annotation.ElementType.LOCAL_VARIABLE, java.lang.annotation.ElementType.ANNOTATION_TYPE, java.lang.annotation.ElementType.PACKAGE, java.lang.annotation.ElementType.TYPE_PARAMETER}) public @interface Method { } diff --git a/src/test/resources/syntheticannotationtarget/expected/com/example/Param.java b/src/test/resources/syntheticannotationtarget/expected/com/example/Param.java index 535453636..7bc8f88ef 100644 --- a/src/test/resources/syntheticannotationtarget/expected/com/example/Param.java +++ b/src/test/resources/syntheticannotationtarget/expected/com/example/Param.java @@ -1,5 +1,5 @@ package com.example; -@java.lang.annotation.Target({ java.lang.annotation.ElementType.PARAMETER, java.lang.annotation.ElementType.TYPE_USE }) +@java.lang.annotation.Target({ java.lang.annotation.ElementType.TYPE_USE, java.lang.annotation.ElementType.TYPE, java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.PARAMETER, java.lang.annotation.ElementType.CONSTRUCTOR, java.lang.annotation.ElementType.LOCAL_VARIABLE, java.lang.annotation.ElementType.ANNOTATION_TYPE, java.lang.annotation.ElementType.PACKAGE, java.lang.annotation.ElementType.TYPE_PARAMETER}) public @interface Param { } diff --git a/src/test/resources/syntheticannotationtarget/expected/com/example/Simple.java b/src/test/resources/syntheticannotationtarget/expected/com/example/Simple.java index 6cee1bfa6..97e264ba3 100644 --- a/src/test/resources/syntheticannotationtarget/expected/com/example/Simple.java +++ b/src/test/resources/syntheticannotationtarget/expected/com/example/Simple.java @@ -1,8 +1,9 @@ package com.example; +import static java.lang.annotation.ElementType.*; + import java.lang.annotation.Target; import java.util.*; -import static java.lang.annotation.ElementType.*; @Type public class Simple<@TypeParam T> { @@ -10,10 +11,8 @@ public class Simple<@TypeParam T> { @Method @AnnoDecl public <@TypeParam U> void baz(@Param U u) { - @LocalVariable - Simple<@TypeParam String> simple = new Simple<>(); - @LocalVariable - int x = simple.field; + @LocalVariable Simple<@TypeParam String> simple = new Simple<>(); + @LocalVariable int x = simple.field; EnumTest e = EnumTest.A; List y; } @@ -27,9 +26,8 @@ public Simple() { } @AnnotationDeclaration - @Target({ METHOD }) - private @interface AnnoDecl { - } + @Target({ METHOD, FIELD, TYPE_USE }) + private @interface AnnoDecl {} enum EnumTest { diff --git a/src/test/resources/syntheticannotationtarget/expected/com/example/Type.java b/src/test/resources/syntheticannotationtarget/expected/com/example/Type.java index f0c6f9946..f4a89c2d6 100644 --- a/src/test/resources/syntheticannotationtarget/expected/com/example/Type.java +++ b/src/test/resources/syntheticannotationtarget/expected/com/example/Type.java @@ -1,5 +1,5 @@ package com.example; -@java.lang.annotation.Target({ java.lang.annotation.ElementType.TYPE, java.lang.annotation.ElementType.TYPE_USE }) +@java.lang.annotation.Target({ java.lang.annotation.ElementType.TYPE_USE, java.lang.annotation.ElementType.TYPE, java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.PARAMETER, java.lang.annotation.ElementType.CONSTRUCTOR, java.lang.annotation.ElementType.LOCAL_VARIABLE, java.lang.annotation.ElementType.ANNOTATION_TYPE, java.lang.annotation.ElementType.PACKAGE, java.lang.annotation.ElementType.TYPE_PARAMETER}) public @interface Type { } diff --git a/src/test/resources/syntheticannotationtarget/expected/com/example/TypeParam.java b/src/test/resources/syntheticannotationtarget/expected/com/example/TypeParam.java index 918f91cad..784f3432d 100644 --- a/src/test/resources/syntheticannotationtarget/expected/com/example/TypeParam.java +++ b/src/test/resources/syntheticannotationtarget/expected/com/example/TypeParam.java @@ -1,5 +1,5 @@ package com.example; -@java.lang.annotation.Target({ java.lang.annotation.ElementType.TYPE_PARAMETER, java.lang.annotation.ElementType.TYPE_USE }) +@java.lang.annotation.Target({ java.lang.annotation.ElementType.TYPE_USE, java.lang.annotation.ElementType.TYPE, java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.PARAMETER, java.lang.annotation.ElementType.CONSTRUCTOR, java.lang.annotation.ElementType.LOCAL_VARIABLE, java.lang.annotation.ElementType.ANNOTATION_TYPE, java.lang.annotation.ElementType.PACKAGE, java.lang.annotation.ElementType.TYPE_PARAMETER}) public @interface TypeParam { } diff --git a/src/test/resources/syntheticenumfromanno/expected/com/example/Anno.java b/src/test/resources/syntheticenumfromanno/expected/com/example/Anno.java new file mode 100644 index 000000000..cf0e69b53 --- /dev/null +++ b/src/test/resources/syntheticenumfromanno/expected/com/example/Anno.java @@ -0,0 +1,7 @@ +package com.example; + +@java.lang.annotation.Target({ java.lang.annotation.ElementType.TYPE_USE, java.lang.annotation.ElementType.TYPE, java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.PARAMETER, java.lang.annotation.ElementType.CONSTRUCTOR, java.lang.annotation.ElementType.LOCAL_VARIABLE, java.lang.annotation.ElementType.ANNOTATION_TYPE, java.lang.annotation.ElementType.PACKAGE, java.lang.annotation.ElementType.TYPE_PARAMETER}) +public @interface Anno { + + public com.example.MyEnum[] myEnum(); +} diff --git a/src/test/resources/syntheticenumfromanno/expected/com/example/MyEnum.java b/src/test/resources/syntheticenumfromanno/expected/com/example/MyEnum.java new file mode 100644 index 000000000..c51247600 --- /dev/null +++ b/src/test/resources/syntheticenumfromanno/expected/com/example/MyEnum.java @@ -0,0 +1,6 @@ +package com.example; + +public enum MyEnum { + FIRST, + SECOND, +} diff --git a/src/test/resources/syntheticenumfromanno/expected/com/example/Simple.java b/src/test/resources/syntheticenumfromanno/expected/com/example/Simple.java new file mode 100644 index 000000000..55a2e0afb --- /dev/null +++ b/src/test/resources/syntheticenumfromanno/expected/com/example/Simple.java @@ -0,0 +1,8 @@ +package com.example; + +public class Simple { + + @Anno(myEnum = { MyEnum.FIRST, MyEnum.SECOND }) + void foo() { + } +} diff --git a/src/test/resources/syntheticenumfromanno/input/com/example/Simple.java b/src/test/resources/syntheticenumfromanno/input/com/example/Simple.java new file mode 100644 index 000000000..be0fe63ac --- /dev/null +++ b/src/test/resources/syntheticenumfromanno/input/com/example/Simple.java @@ -0,0 +1,8 @@ +package com.example; + +public class Simple { + @Anno(myEnum = { MyEnum.FIRST, MyEnum.SECOND }) + void foo() { + + } +} diff --git a/src/test/resources/syntheticmethodrefs/expected/com/example/Bar.java b/src/test/resources/syntheticmethodrefs/expected/com/example/Bar.java index dabc56d57..e3748415a 100644 --- a/src/test/resources/syntheticmethodrefs/expected/com/example/Bar.java +++ b/src/test/resources/syntheticmethodrefs/expected/com/example/Bar.java @@ -1,7 +1,7 @@ package com.example; -import java.util.Map; import java.util.List; +import java.util.Map; public class Bar { diff --git a/src/test/resources/syntheticmethodrefs/expected/com/example/Foo.java b/src/test/resources/syntheticmethodrefs/expected/com/example/Foo.java index aec1b3b86..8efa51127 100644 --- a/src/test/resources/syntheticmethodrefs/expected/com/example/Foo.java +++ b/src/test/resources/syntheticmethodrefs/expected/com/example/Foo.java @@ -2,35 +2,35 @@ public class Foo { - public static ComExampleFooMapListReturnType mapList(java.lang.Runnable parameter0) { + public static com.example.ComExampleFooMapList8ReturnType mapList8(com.example.SyntheticFunction3 parameter0) { throw new java.lang.Error(); } - public static ComExampleFooMapList2ReturnType mapList2(java.util.function.Supplier parameter0) { + public static com.example.ComExampleFooMapList7ReturnType mapList7(com.example.SyntheticConsumer3>, ?> parameter0) { throw new java.lang.Error(); } - public static ComExampleFooMapList3ReturnType mapList3(java.util.function.Consumer parameter0) { + public static com.example.ComExampleFooMapList6ReturnType mapList6(java.util.function.BiFunction parameter0) { throw new java.lang.Error(); } - public static ComExampleFooMapList4ReturnType mapList4(java.util.function.Function, ?> parameter0) { + public static com.example.ComExampleFooMapList5ReturnType mapList5(java.util.function.BiConsumer parameter0) { throw new java.lang.Error(); } - public static ComExampleFooMapList5ReturnType mapList5(java.util.function.BiConsumer parameter0) { + public static com.example.ComExampleFooMapList4ReturnType mapList4(java.util.function.Function, ?> parameter0) { throw new java.lang.Error(); } - public static ComExampleFooMapList6ReturnType mapList6(java.util.function.BiFunction parameter0) { + public static com.example.ComExampleFooMapList3ReturnType mapList3(java.util.function.Consumer parameter0) { throw new java.lang.Error(); } - public static ComExampleFooMapList7ReturnType mapList7(SyntheticConsumer3>, Object> parameter0) { + public static com.example.ComExampleFooMapList2ReturnType mapList2(java.util.function.Supplier parameter0) { throw new java.lang.Error(); } - public static ComExampleFooMapList8ReturnType mapList8(SyntheticFunction3 parameter0) { + public static com.example.ComExampleFooMapListReturnType mapList(java.lang.Runnable parameter0) { throw new java.lang.Error(); } } diff --git a/src/test/resources/syntheticmethodrefs/expected/com/example/SyntheticConsumer3.java b/src/test/resources/syntheticmethodrefs/expected/com/example/SyntheticConsumer3.java index 17cb7f7ec..22c2c2c73 100644 --- a/src/test/resources/syntheticmethodrefs/expected/com/example/SyntheticConsumer3.java +++ b/src/test/resources/syntheticmethodrefs/expected/com/example/SyntheticConsumer3.java @@ -1,5 +1,6 @@ package com.example; +@FunctionalInterface public interface SyntheticConsumer3 { public void apply(T parameter0, T1 parameter1, T2 parameter2); diff --git a/src/test/resources/syntheticmethodrefs/expected/com/example/SyntheticFunction3.java b/src/test/resources/syntheticmethodrefs/expected/com/example/SyntheticFunction3.java index bfe5837e8..ecc49a8a0 100644 --- a/src/test/resources/syntheticmethodrefs/expected/com/example/SyntheticFunction3.java +++ b/src/test/resources/syntheticmethodrefs/expected/com/example/SyntheticFunction3.java @@ -1,5 +1,6 @@ package com.example; +@FunctionalInterface public interface SyntheticFunction3 { public T3 apply(T parameter0, T1 parameter1, T2 parameter2); diff --git a/src/test/resources/syntheticsuperlub/expected/com/example/Dog.java b/src/test/resources/syntheticsuperlub/expected/com/example/Dog.java index 46baf2c1a..f7614905b 100644 --- a/src/test/resources/syntheticsuperlub/expected/com/example/Dog.java +++ b/src/test/resources/syntheticsuperlub/expected/com/example/Dog.java @@ -1,7 +1,8 @@ package com.example; + import org.wild.Mammal; -import org.wild.WebbedPaws; import org.wild.RegularPaws; +import org.wild.WebbedPaws; public class Dog extends Mammal { public void setup(String breed) { diff --git a/src/test/resources/syntheticsuperlub/expected/com/example/SyntheticTypeForPaws.java b/src/test/resources/syntheticsuperlub/expected/com/example/SyntheticTypeForPaws.java index 94228aa82..2a2a5abb1 100644 --- a/src/test/resources/syntheticsuperlub/expected/com/example/SyntheticTypeForPaws.java +++ b/src/test/resources/syntheticsuperlub/expected/com/example/SyntheticTypeForPaws.java @@ -1,7 +1,7 @@ package com.example; public class SyntheticTypeForPaws { - public SetNumberReturnType setNumber(int parameter0) { + public com.example.SetNumberReturnType setNumber(int parameter0) { throw new java.lang.Error(); } } \ No newline at end of file diff --git a/src/test/resources/syntheticsuperundeclaredvariables2/expected/org/wild/Mammal.java b/src/test/resources/syntheticsuperundeclaredvariables2/expected/org/wild/Mammal.java index 9f440c163..e3d19d6eb 100644 --- a/src/test/resources/syntheticsuperundeclaredvariables2/expected/org/wild/Mammal.java +++ b/src/test/resources/syntheticsuperundeclaredvariables2/expected/org/wild/Mammal.java @@ -1,8 +1,5 @@ package org.wild; public class Mammal { - public boolean bornFromEggs; - public int x; - } diff --git a/src/test/resources/syntheticsupervariables/expected/org/wild/Mammal.java b/src/test/resources/syntheticsupervariables/expected/org/wild/Mammal.java index 96dc33b2a..c31b0a854 100644 --- a/src/test/resources/syntheticsupervariables/expected/org/wild/Mammal.java +++ b/src/test/resources/syntheticsupervariables/expected/org/wild/Mammal.java @@ -1,7 +1,5 @@ package org.wild; public class Mammal { - - public String habitat; - public boolean canBreathUnderWater; + public java.lang.String habitat; } diff --git a/src/test/resources/trywithresources/expected/com/example/Simple.java b/src/test/resources/trywithresources/expected/com/example/Simple.java index d3ebc5a7a..a0e1381eb 100644 --- a/src/test/resources/trywithresources/expected/com/example/Simple.java +++ b/src/test/resources/trywithresources/expected/com/example/Simple.java @@ -1,19 +1,16 @@ package com.example; -import org.example.Resource; import org.example.OtherResource; +import org.example.Resource; import org.example.ThirdResource; class Simple { - private final ThirdResource r = null; + private final ThirdResource r = null; - void bar(final OtherResource o) throws Exception { - try (Resource r = new Resource()) { - } - try (o) { - } - try (r) { - } - } + void bar(final OtherResource o) throws Exception { + try (Resource r = new Resource()) {} + try (o) {} + try (r) {} + } } diff --git a/src/test/resources/twotypecorrect/expected/com/example/Simple.java b/src/test/resources/twotypecorrect/expected/com/example/Simple.java index d0ecfaa0f..69a1da78b 100644 --- a/src/test/resources/twotypecorrect/expected/com/example/Simple.java +++ b/src/test/resources/twotypecorrect/expected/com/example/Simple.java @@ -1,8 +1,8 @@ package com.example; -import org.example.Type; -import org.example.PrimitiveType; import org.example.ClassOrInterfaceType; +import org.example.PrimitiveType; +import org.example.Type; class Simple { void bar(Type t) { diff --git a/src/test/resources/twotypecorrect2/expected/com/example/Simple.java b/src/test/resources/twotypecorrect2/expected/com/example/Simple.java index a44b49cff..21706982c 100644 --- a/src/test/resources/twotypecorrect2/expected/com/example/Simple.java +++ b/src/test/resources/twotypecorrect2/expected/com/example/Simple.java @@ -1,8 +1,8 @@ package com.example; -import org.example.Type; -import org.example.PrimitiveType; import org.example.ClassOrInterfaceType; +import org.example.PrimitiveType; +import org.example.Type; class Simple { void bar(Type t) { diff --git a/src/test/resources/typeargumentsuperrelationship/expected/com/example/BadFoo.java b/src/test/resources/typeargumentsuperrelationship/expected/com/example/BadFoo.java new file mode 100644 index 000000000..0959a2ed3 --- /dev/null +++ b/src/test/resources/typeargumentsuperrelationship/expected/com/example/BadFoo.java @@ -0,0 +1,7 @@ +package com.example; +public class BadFoo { + + public BadFoo() { + throw new java.lang.Error(); + } +} diff --git a/src/test/resources/unsolvableinterface/expected/com/example/Baz.java b/src/test/resources/typeargumentsuperrelationship/expected/com/example/Bar.java similarity index 50% rename from src/test/resources/unsolvableinterface/expected/com/example/Baz.java rename to src/test/resources/typeargumentsuperrelationship/expected/com/example/Bar.java index 7bb4a7b04..39bcf717e 100644 --- a/src/test/resources/unsolvableinterface/expected/com/example/Baz.java +++ b/src/test/resources/typeargumentsuperrelationship/expected/com/example/Bar.java @@ -1,4 +1,3 @@ package com.example; - -public class Baz { +public class Bar { } diff --git a/src/test/resources/typeargumentsuperrelationship/expected/com/example/Bar2.java b/src/test/resources/typeargumentsuperrelationship/expected/com/example/Bar2.java new file mode 100644 index 000000000..dd38f1955 --- /dev/null +++ b/src/test/resources/typeargumentsuperrelationship/expected/com/example/Bar2.java @@ -0,0 +1,3 @@ +package com.example; +public class Bar2 extends com.example.Bar { +} diff --git a/src/test/resources/typeargumentsuperrelationship/expected/com/example/Bar3.java b/src/test/resources/typeargumentsuperrelationship/expected/com/example/Bar3.java new file mode 100644 index 000000000..74118a32a --- /dev/null +++ b/src/test/resources/typeargumentsuperrelationship/expected/com/example/Bar3.java @@ -0,0 +1,3 @@ +package com.example; +public class Bar3 { +} diff --git a/src/test/resources/typeargumentsuperrelationship/expected/com/example/Baz.java b/src/test/resources/typeargumentsuperrelationship/expected/com/example/Baz.java new file mode 100644 index 000000000..bbec949f4 --- /dev/null +++ b/src/test/resources/typeargumentsuperrelationship/expected/com/example/Baz.java @@ -0,0 +1,3 @@ +package com.example; +public class Baz extends com.example.Bar { +} diff --git a/src/test/resources/typeargumentsuperrelationship/expected/com/example/Baz2.java b/src/test/resources/typeargumentsuperrelationship/expected/com/example/Baz2.java new file mode 100644 index 000000000..3135e9956 --- /dev/null +++ b/src/test/resources/typeargumentsuperrelationship/expected/com/example/Baz2.java @@ -0,0 +1,3 @@ +package com.example; +public class Baz2 extends com.example.Bar2 { +} diff --git a/src/test/resources/typeargumentsuperrelationship/expected/com/example/Baz3.java b/src/test/resources/typeargumentsuperrelationship/expected/com/example/Baz3.java new file mode 100644 index 000000000..d6d9209ee --- /dev/null +++ b/src/test/resources/typeargumentsuperrelationship/expected/com/example/Baz3.java @@ -0,0 +1,3 @@ +package com.example; +public class Baz3 extends com.example.Bar3 { +} diff --git a/src/test/resources/typeargumentsuperrelationship/expected/com/example/Foo.java b/src/test/resources/typeargumentsuperrelationship/expected/com/example/Foo.java new file mode 100644 index 000000000..1faaf4e2e --- /dev/null +++ b/src/test/resources/typeargumentsuperrelationship/expected/com/example/Foo.java @@ -0,0 +1,7 @@ +package com.example; +public class Foo extends com.example.BadFoo { + + public Foo() { + throw new java.lang.Error(); + } +} diff --git a/src/test/resources/typeargumentsuperrelationship/expected/com/example/Simple.java b/src/test/resources/typeargumentsuperrelationship/expected/com/example/Simple.java new file mode 100644 index 000000000..246dff17f --- /dev/null +++ b/src/test/resources/typeargumentsuperrelationship/expected/com/example/Simple.java @@ -0,0 +1,12 @@ +package com.example; + +public class Simple { + + void bar() { + Foo foo = new Foo(); + Foo foo2 = new Foo(); + BadFoo badFoo = new BadFoo(); + foo = foo2; + badFoo = new Foo<>(); + } +} diff --git a/src/test/resources/typeargumentsuperrelationship/input/com/example/Simple.java b/src/test/resources/typeargumentsuperrelationship/input/com/example/Simple.java new file mode 100644 index 000000000..d2835a98c --- /dev/null +++ b/src/test/resources/typeargumentsuperrelationship/input/com/example/Simple.java @@ -0,0 +1,15 @@ +package com.example; + +public class Simple { + void bar() { + Foo foo = new Foo(); + Foo foo2 = new Foo(); + + BadFoo badFoo = new BadFoo(); + + // Bar2 should extend Bar + foo = foo2; + // Foo should extend BadFoo, but Bar3 should have no super/subtypes + badFoo = new Foo<>(); + } +} diff --git a/src/test/resources/typevariablemethodscope/expected/org/testing/UnsolvedType.java b/src/test/resources/typevariablemethodscope/expected/org/testing/UnsolvedType.java index cec1d9fa6..cbfe27064 100644 --- a/src/test/resources/typevariablemethodscope/expected/org/testing/UnsolvedType.java +++ b/src/test/resources/typevariablemethodscope/expected/org/testing/UnsolvedType.java @@ -2,7 +2,7 @@ public class UnsolvedType { - public PrintReturnType print() { + public org.testing.PrintReturnType print() { throw new java.lang.Error(); } } diff --git a/src/test/resources/unaryexpr/expected/com/example/Foo.java b/src/test/resources/unaryexpr/expected/com/example/Foo.java index 9748ab885..5ebfc32a7 100644 --- a/src/test/resources/unaryexpr/expected/com/example/Foo.java +++ b/src/test/resources/unaryexpr/expected/com/example/Foo.java @@ -2,15 +2,15 @@ public class Foo { - public int bar; - public long qux; - public int baz() { + public int bar; + + public double razz() { throw new java.lang.Error(); } - public double razz() { + public int baz() { throw new java.lang.Error(); } } \ No newline at end of file diff --git a/src/test/resources/uncatchthrow/expected/org/testing/MyException.java b/src/test/resources/uncatchthrow/expected/org/testing/MyException.java index df912a35a..1e9d11639 100644 --- a/src/test/resources/uncatchthrow/expected/org/testing/MyException.java +++ b/src/test/resources/uncatchthrow/expected/org/testing/MyException.java @@ -1,6 +1,6 @@ package org.testing; -public class MyException extends java.lang.RuntimeException { +public class MyException extends java.lang.Error { public MyException(java.lang.String parameter0) { throw new java.lang.Error(); diff --git a/src/test/resources/uniontype/expected/com/github/javaparser/resolution/UnsolvedSymbolException.java b/src/test/resources/uniontype/expected/com/github/javaparser/resolution/UnsolvedSymbolException.java index 2e40995a7..51c600864 100644 --- a/src/test/resources/uniontype/expected/com/github/javaparser/resolution/UnsolvedSymbolException.java +++ b/src/test/resources/uniontype/expected/com/github/javaparser/resolution/UnsolvedSymbolException.java @@ -1,6 +1,6 @@ package com.github.javaparser.resolution; -public class UnsolvedSymbolException extends Exception { +public class UnsolvedSymbolException extends java.lang.Exception { public UnsolvedSymbolException() { throw new java.lang.Error(); diff --git a/src/test/resources/uniontype/expected/javalanguage/Method.java b/src/test/resources/uniontype/expected/javalanguage/Method.java index 8d7406be1..400d1dece 100644 --- a/src/test/resources/uniontype/expected/javalanguage/Method.java +++ b/src/test/resources/uniontype/expected/javalanguage/Method.java @@ -2,11 +2,11 @@ public class Method { - public Method() { + public javalanguage.SolveReturnType solve() { throw new java.lang.Error(); } - public SolveReturnType solve() { + public Method() { throw new java.lang.Error(); } } diff --git a/src/test/resources/unsolvableinterface/expected/com/example/Foo.java b/src/test/resources/unsolvableinterface/expected/com/example/Foo.java deleted file mode 100644 index 33952969e..000000000 --- a/src/test/resources/unsolvableinterface/expected/com/example/Foo.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.example; - -class Foo extends Baz { - - public void bar() { - System.out.println("Foo is doing something!"); - } -} diff --git a/src/test/resources/unsolvableinterface/input/com/example/Foo.java b/src/test/resources/unsolvableinterface/input/com/example/Foo.java deleted file mode 100644 index 7481e7424..000000000 --- a/src/test/resources/unsolvableinterface/input/com/example/Foo.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.example; - -import java.util.List; - -// Suppose this is an isolated Java program from a real life project, and all of the implementation -// for methods from List are in Baz, then Specimin should simply remove List from the class -// declaration. -class Foo extends Baz implements List { - public void bar() { - System.out.println("Foo is doing something!"); - } - -} diff --git a/src/test/resources/unsolvedexception/expected/com/example/CustomException.java b/src/test/resources/unsolvedexception/expected/com/example/CustomException.java index ce4be9d5a..b9e1b9ab5 100644 --- a/src/test/resources/unsolvedexception/expected/com/example/CustomException.java +++ b/src/test/resources/unsolvedexception/expected/com/example/CustomException.java @@ -1,6 +1,6 @@ package com.example; -public class CustomException extends Exception { +public class CustomException extends java.lang.Exception { public CustomException(java.lang.String parameter0) { throw new java.lang.Error(); diff --git a/src/test/resources/unsolvedfieldsinexistingclass/expected/org/testing/Unsolved.java b/src/test/resources/unsolvedfieldsinexistingclass/expected/org/testing/Unsolved.java index e3fe3e1ee..20c0cb9d1 100644 --- a/src/test/resources/unsolvedfieldsinexistingclass/expected/org/testing/Unsolved.java +++ b/src/test/resources/unsolvedfieldsinexistingclass/expected/org/testing/Unsolved.java @@ -2,7 +2,7 @@ public class Unsolved { - public GetValueReturnType getValue() { + public org.testing.GetValueReturnType getValue() { throw new java.lang.Error(); } } diff --git a/src/test/resources/unsolvedlambdaparameteruse/expected/com/example/Bar.java b/src/test/resources/unsolvedlambdaparameteruse/expected/com/example/Bar.java new file mode 100644 index 000000000..8ce23aeba --- /dev/null +++ b/src/test/resources/unsolvedlambdaparameteruse/expected/com/example/Bar.java @@ -0,0 +1,7 @@ +package com.example; +public class Bar { + + public static com.example.ComExampleBarFooReturnType foo(java.util.function.Consumer parameter0) { + throw new java.lang.Error(); + } +} diff --git a/src/test/resources/unsolvedlambdaparameteruse/expected/com/example/ComExampleBarFooReturnType.java b/src/test/resources/unsolvedlambdaparameteruse/expected/com/example/ComExampleBarFooReturnType.java new file mode 100644 index 000000000..8d573da7b --- /dev/null +++ b/src/test/resources/unsolvedlambdaparameteruse/expected/com/example/ComExampleBarFooReturnType.java @@ -0,0 +1,3 @@ +package com.example; +public class ComExampleBarFooReturnType { +} diff --git a/src/test/resources/unsolvedlambdaparameteruse/expected/com/example/Foo.java b/src/test/resources/unsolvedlambdaparameteruse/expected/com/example/Foo.java new file mode 100644 index 000000000..3d218e295 --- /dev/null +++ b/src/test/resources/unsolvedlambdaparameteruse/expected/com/example/Foo.java @@ -0,0 +1,12 @@ +package com.example; + +public class Foo { + + public void bar() { + Bar.foo( + (param) -> { + int x = param.getX(); + int y = param.y; + }); + } +} diff --git a/src/test/resources/unsolvedlambdaparameteruse/expected/com/example/SyntheticTypeForParam.java b/src/test/resources/unsolvedlambdaparameteruse/expected/com/example/SyntheticTypeForParam.java new file mode 100644 index 000000000..1c1211388 --- /dev/null +++ b/src/test/resources/unsolvedlambdaparameteruse/expected/com/example/SyntheticTypeForParam.java @@ -0,0 +1,8 @@ +package com.example; +public class SyntheticTypeForParam { + public int y; + + public int getX() { + throw new java.lang.Error(); + } +} diff --git a/src/test/resources/unsolvedlambdaparameteruse/input/com/example/Foo.java b/src/test/resources/unsolvedlambdaparameteruse/input/com/example/Foo.java new file mode 100644 index 000000000..9ae06fc1b --- /dev/null +++ b/src/test/resources/unsolvedlambdaparameteruse/input/com/example/Foo.java @@ -0,0 +1,10 @@ +package com.example; + +public class Foo { + public void bar() { + Bar.foo((param) -> { + int x = param.getX(); + int y = param.y; + }); + } +} diff --git a/src/test/resources/unsolvedmethodreferencewithknownlhs/expected/com/example/Foo.java b/src/test/resources/unsolvedmethodreferencewithknownlhs/expected/com/example/Foo.java new file mode 100644 index 000000000..dac657048 --- /dev/null +++ b/src/test/resources/unsolvedmethodreferencewithknownlhs/expected/com/example/Foo.java @@ -0,0 +1,18 @@ +package com.example; +public class Foo { + public java.lang.Boolean myMethod4() { + throw new java.lang.Error(); + } + + public java.lang.String myMethod3() { + throw new java.lang.Error(); + } + + public static java.lang.Number myMethod2(java.lang.String parameter0) { + throw new java.lang.Error(); + } + + public java.lang.Object myMethod(java.lang.String parameter0) { + throw new java.lang.Error(); + } +} diff --git a/src/test/resources/unsolvedmethodreferencewithknownlhs/expected/com/example/Simple.java b/src/test/resources/unsolvedmethodreferencewithknownlhs/expected/com/example/Simple.java new file mode 100644 index 000000000..4fa540124 --- /dev/null +++ b/src/test/resources/unsolvedmethodreferencewithknownlhs/expected/com/example/Simple.java @@ -0,0 +1,23 @@ +package com.example; + +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.function.Predicate; + +public class Simple { + public void foo() { + bar(Foo::myMethod); + baz(Foo::myMethod2); + + Function func = Foo::myMethod3; + Predicate pred = Foo::myMethod4; + } + + void bar(BiFunction func) { + throw new java.lang.Error(); + } + + void baz(Function func) { + throw new java.lang.Error(); + } +} \ No newline at end of file diff --git a/src/test/resources/unsolvedmethodreferencewithknownlhs/input/com/example/Simple.java b/src/test/resources/unsolvedmethodreferencewithknownlhs/input/com/example/Simple.java new file mode 100644 index 000000000..4fa540124 --- /dev/null +++ b/src/test/resources/unsolvedmethodreferencewithknownlhs/input/com/example/Simple.java @@ -0,0 +1,23 @@ +package com.example; + +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.function.Predicate; + +public class Simple { + public void foo() { + bar(Foo::myMethod); + baz(Foo::myMethod2); + + Function func = Foo::myMethod3; + Predicate pred = Foo::myMethod4; + } + + void bar(BiFunction func) { + throw new java.lang.Error(); + } + + void baz(Function func) { + throw new java.lang.Error(); + } +} \ No newline at end of file diff --git a/src/test/resources/unsolvedmethodtypecorrect/expected/com/example/Simple.java b/src/test/resources/unsolvedmethodtypecorrect/expected/com/example/Simple.java index c72969ea2..df2901d9d 100644 --- a/src/test/resources/unsolvedmethodtypecorrect/expected/com/example/Simple.java +++ b/src/test/resources/unsolvedmethodtypecorrect/expected/com/example/Simple.java @@ -1,15 +1,12 @@ package com.example; import org.example.Foo; +import org.example.LocalVariables; class Simple { void bar() { Foo f = new Foo(); f.getLocals().set(0); - baz(); - } - - void baz() { - throw new java.lang.Error(); + final LocalVariables locals = f.getLocals(); } } diff --git a/src/test/resources/unsolvedmethodtypecorrect/expected/org/example/Foo.java b/src/test/resources/unsolvedmethodtypecorrect/expected/org/example/Foo.java index 746406f0c..635a3a1b7 100644 --- a/src/test/resources/unsolvedmethodtypecorrect/expected/org/example/Foo.java +++ b/src/test/resources/unsolvedmethodtypecorrect/expected/org/example/Foo.java @@ -2,11 +2,11 @@ public class Foo { - public Foo() { + public org.example.LocalVariables getLocals() { throw new java.lang.Error(); } - public org.example.LocalVariables getLocals() { + public Foo() { throw new java.lang.Error(); } } diff --git a/src/test/resources/unsolvedmethodtypecorrect/expected/org/example/LocalVariables.java b/src/test/resources/unsolvedmethodtypecorrect/expected/org/example/LocalVariables.java index 148da3345..11a58ae71 100644 --- a/src/test/resources/unsolvedmethodtypecorrect/expected/org/example/LocalVariables.java +++ b/src/test/resources/unsolvedmethodtypecorrect/expected/org/example/LocalVariables.java @@ -2,7 +2,7 @@ public class LocalVariables { - public SetReturnType set(int parameter0) { + public org.example.SetReturnType set(int parameter0) { throw new java.lang.Error(); } } \ No newline at end of file diff --git a/src/test/resources/unsolvedmethodtypecorrect/input/com/example/Simple.java b/src/test/resources/unsolvedmethodtypecorrect/input/com/example/Simple.java index d68326a0d..ce3640475 100644 --- a/src/test/resources/unsolvedmethodtypecorrect/input/com/example/Simple.java +++ b/src/test/resources/unsolvedmethodtypecorrect/input/com/example/Simple.java @@ -8,12 +8,6 @@ class Simple { void bar() { Foo f = new Foo(); f.getLocals().set(0); - baz(); - } - - void baz() { - // To trigger JavaTypeCorrect. - Foo f = new Foo(); final LocalVariables locals = f.getLocals(); } } diff --git a/src/test/resources/unsolvedmethodwitharrayparameter/expected/org/testing/UnsolvedType.java b/src/test/resources/unsolvedmethodwitharrayparameter/expected/org/testing/UnsolvedType.java index 04ab104c6..6aca9001d 100644 --- a/src/test/resources/unsolvedmethodwitharrayparameter/expected/org/testing/UnsolvedType.java +++ b/src/test/resources/unsolvedmethodwitharrayparameter/expected/org/testing/UnsolvedType.java @@ -2,7 +2,7 @@ public class UnsolvedType { - public static OrgTestingUnsolvedTypeProcessArrayReturnType processArray(int[][][] parameter0) { + public static org.testing.OrgTestingUnsolvedTypeProcessArrayReturnType processArray(int[][][] parameter0) { throw new java.lang.Error(); } } diff --git a/src/test/resources/unsolvednonstaticfield/expected/org/sampling/Baz.java b/src/test/resources/unsolvednonstaticfield/expected/org/sampling/Baz.java index 6661af5e8..621802d52 100644 --- a/src/test/resources/unsolvednonstaticfield/expected/org/sampling/Baz.java +++ b/src/test/resources/unsolvednonstaticfield/expected/org/sampling/Baz.java @@ -2,7 +2,7 @@ public class Baz { - public OrgSamplingBazCalSyntheticType cal; + public org.sampling.OrgSamplingBazCalSyntheticType cal; public Baz() { throw new java.lang.Error(); diff --git a/src/test/resources/unsolvednonstaticfield/expected/org/sampling/OrgSamplingBazCalSyntheticType.java b/src/test/resources/unsolvednonstaticfield/expected/org/sampling/OrgSamplingBazCalSyntheticType.java index 11eaf87f8..1b6e35cf7 100644 --- a/src/test/resources/unsolvednonstaticfield/expected/org/sampling/OrgSamplingBazCalSyntheticType.java +++ b/src/test/resources/unsolvednonstaticfield/expected/org/sampling/OrgSamplingBazCalSyntheticType.java @@ -2,7 +2,7 @@ public class OrgSamplingBazCalSyntheticType { - public DoAdditionReturnType doAddition() { + public org.sampling.DoAdditionReturnType doAddition() { throw new java.lang.Error(); } } diff --git a/src/test/resources/unsolvedoverrideannotation/expected/org/testing/Baz.java b/src/test/resources/unsolvedoverrideannotation/expected/org/testing/Baz.java index 81822f42a..8843945f4 100644 --- a/src/test/resources/unsolvedoverrideannotation/expected/org/testing/Baz.java +++ b/src/test/resources/unsolvedoverrideannotation/expected/org/testing/Baz.java @@ -2,7 +2,7 @@ public class Baz { - void bar() { + void bar() { throw new java.lang.Error(); } } diff --git a/src/test/resources/unsolvedparameter/expected/com/example/Simple.java b/src/test/resources/unsolvedparameter/expected/com/example/Simple.java index 4f7d709d5..3938e40af 100644 --- a/src/test/resources/unsolvedparameter/expected/com/example/Simple.java +++ b/src/test/resources/unsolvedparameter/expected/com/example/Simple.java @@ -1,8 +1,8 @@ package com.example; import com.name.FullName; -import com.name.MiddleName; import com.name.LastName; +import com.name.MiddleName; public class Simple { diff --git a/src/test/resources/unsolvedstaticmethod/expected/com/example/MyClass.java b/src/test/resources/unsolvedstaticmethod/expected/com/example/MyClass.java index 7a6d5ad39..b639f0bd8 100644 --- a/src/test/resources/unsolvedstaticmethod/expected/com/example/MyClass.java +++ b/src/test/resources/unsolvedstaticmethod/expected/com/example/MyClass.java @@ -2,7 +2,7 @@ public class MyClass { - public static ComExampleMyClassProcessReturnType process(int parameter0) { + public static com.example.ComExampleMyClassProcessReturnType process(int parameter0) { throw new java.lang.Error(); } } diff --git a/src/test/resources/unsolvedstaticmethod/expected/org/testing/ThisClass.java b/src/test/resources/unsolvedstaticmethod/expected/org/testing/ThisClass.java index 04add564a..8b9fb6b28 100644 --- a/src/test/resources/unsolvedstaticmethod/expected/org/testing/ThisClass.java +++ b/src/test/resources/unsolvedstaticmethod/expected/org/testing/ThisClass.java @@ -2,7 +2,7 @@ public class ThisClass { - public static OrgTestingThisClassProcessReturnType process(java.lang.String parameter0, unreal.pack.AClass parameter1) { + public static org.testing.OrgTestingThisClassProcessReturnType process(java.lang.String parameter0, unreal.pack.AClass parameter1) { throw new java.lang.Error(); } } diff --git a/src/test/resources/unsolvedstaticqualifiedfield/expected/org/sampling/Baz.java b/src/test/resources/unsolvedstaticqualifiedfield/expected/org/sampling/Baz.java index 4d091a8b7..ca3fde3ab 100644 --- a/src/test/resources/unsolvedstaticqualifiedfield/expected/org/sampling/Baz.java +++ b/src/test/resources/unsolvedstaticqualifiedfield/expected/org/sampling/Baz.java @@ -2,5 +2,5 @@ public class Baz { - public static OrgSamplingBazMyFieldSyntheticType myField; + public static org.sampling.OrgSamplingBazMyFieldSyntheticType myField; } diff --git a/src/test/resources/unsolvedstaticqualifiedfield/expected/org/sampling/OrgSamplingBazMyFieldSyntheticType.java b/src/test/resources/unsolvedstaticqualifiedfield/expected/org/sampling/OrgSamplingBazMyFieldSyntheticType.java index e7e777c83..5b129c12a 100644 --- a/src/test/resources/unsolvedstaticqualifiedfield/expected/org/sampling/OrgSamplingBazMyFieldSyntheticType.java +++ b/src/test/resources/unsolvedstaticqualifiedfield/expected/org/sampling/OrgSamplingBazMyFieldSyntheticType.java @@ -2,7 +2,7 @@ public class OrgSamplingBazMyFieldSyntheticType { - public DoAdditionReturnType doAddition() { + public org.sampling.DoAdditionReturnType doAddition() { throw new java.lang.Error(); } } diff --git a/src/test/resources/unsolvedstaticsimplefield/expected/org/sampling/Baz.java b/src/test/resources/unsolvedstaticsimplefield/expected/org/sampling/Baz.java index 4d091a8b7..ca3fde3ab 100644 --- a/src/test/resources/unsolvedstaticsimplefield/expected/org/sampling/Baz.java +++ b/src/test/resources/unsolvedstaticsimplefield/expected/org/sampling/Baz.java @@ -2,5 +2,5 @@ public class Baz { - public static OrgSamplingBazMyFieldSyntheticType myField; + public static org.sampling.OrgSamplingBazMyFieldSyntheticType myField; } diff --git a/src/test/resources/unsolvedstaticsimplefield/expected/org/sampling/OrgSamplingBazMyFieldSyntheticType.java b/src/test/resources/unsolvedstaticsimplefield/expected/org/sampling/OrgSamplingBazMyFieldSyntheticType.java index e7e777c83..5b129c12a 100644 --- a/src/test/resources/unsolvedstaticsimplefield/expected/org/sampling/OrgSamplingBazMyFieldSyntheticType.java +++ b/src/test/resources/unsolvedstaticsimplefield/expected/org/sampling/OrgSamplingBazMyFieldSyntheticType.java @@ -2,7 +2,7 @@ public class OrgSamplingBazMyFieldSyntheticType { - public DoAdditionReturnType doAddition() { + public org.sampling.DoAdditionReturnType doAddition() { throw new java.lang.Error(); } } diff --git a/src/test/resources/unsolvedsuperconstructor/expected/com/example/Bar.java b/src/test/resources/unsolvedsuperconstructor/expected/com/example/Bar.java new file mode 100644 index 000000000..3a7686f36 --- /dev/null +++ b/src/test/resources/unsolvedsuperconstructor/expected/com/example/Bar.java @@ -0,0 +1,7 @@ +package com.example; + +public class Bar { + public Bar(java.lang.String parameter0) { + throw new java.lang.Error(); + } +} diff --git a/src/test/resources/unsolvedsuperconstructor/expected/com/example/Foo.java b/src/test/resources/unsolvedsuperconstructor/expected/com/example/Foo.java new file mode 100644 index 000000000..ad86d97be --- /dev/null +++ b/src/test/resources/unsolvedsuperconstructor/expected/com/example/Foo.java @@ -0,0 +1,11 @@ +package com.example; + +class Foo extends Bar { + public Foo() { + super("any argument"); + } + + public static Bar bar() { + return new Bar("a consistent argument"); + } +} \ No newline at end of file diff --git a/src/test/resources/unsolvedsuperconstructor/input/com/example/Foo.java b/src/test/resources/unsolvedsuperconstructor/input/com/example/Foo.java new file mode 100644 index 000000000..fb78cdda4 --- /dev/null +++ b/src/test/resources/unsolvedsuperconstructor/input/com/example/Foo.java @@ -0,0 +1,13 @@ +package com.example; + +class Foo extends Bar { + // Target + public Foo() { + super("any argument"); + } + + // Also target + public static Bar bar() { + return new Bar("a consistent argument"); + } +} \ No newline at end of file diff --git a/src/test/resources/unsolvedsuperconstructor2/expected/com/example/Bar.java b/src/test/resources/unsolvedsuperconstructor2/expected/com/example/Bar.java new file mode 100644 index 000000000..9d94f8e65 --- /dev/null +++ b/src/test/resources/unsolvedsuperconstructor2/expected/com/example/Bar.java @@ -0,0 +1,11 @@ +package com.example; + +public class Bar { + public Bar(java.lang.String parameter0) { + throw new java.lang.Error(); + } + + public Bar(java.lang.String parameter0, java.lang.String parameter1) { + throw new java.lang.Error(); + } +} diff --git a/src/test/resources/unsolvedsuperconstructor2/expected/com/example/Foo.java b/src/test/resources/unsolvedsuperconstructor2/expected/com/example/Foo.java new file mode 100644 index 000000000..ca89c4ec2 --- /dev/null +++ b/src/test/resources/unsolvedsuperconstructor2/expected/com/example/Foo.java @@ -0,0 +1,12 @@ +package com.example; + +class Foo extends Bar { + public Foo() { + super("any argument", "some other argument"); + } + + public static Bar bar() { + Foo foo = new Foo(); + return new Bar("a consistent argument"); + } +} \ No newline at end of file diff --git a/src/test/resources/unsolvedsuperconstructor2/input/com/example/Foo.java b/src/test/resources/unsolvedsuperconstructor2/input/com/example/Foo.java new file mode 100644 index 000000000..ca89c4ec2 --- /dev/null +++ b/src/test/resources/unsolvedsuperconstructor2/input/com/example/Foo.java @@ -0,0 +1,12 @@ +package com.example; + +class Foo extends Bar { + public Foo() { + super("any argument", "some other argument"); + } + + public static Bar bar() { + Foo foo = new Foo(); + return new Bar("a consistent argument"); + } +} \ No newline at end of file diff --git a/src/test/resources/unusedfuncinterfacemethod/expected/com/example/Bar.java b/src/test/resources/unusedfuncinterfacemethod/expected/com/example/Bar.java index 0e96b9b23..5e2c0a859 100644 --- a/src/test/resources/unusedfuncinterfacemethod/expected/com/example/Bar.java +++ b/src/test/resources/unusedfuncinterfacemethod/expected/com/example/Bar.java @@ -2,7 +2,7 @@ public class Bar { - public static ComExampleBarUseReturnType use(SyntheticFunction4 parameter0) { + public static com.example.ComExampleBarUseReturnType use(com.example.SyntheticFunction4 parameter0) { throw new java.lang.Error(); } } diff --git a/src/test/resources/unusedfuncinterfacemethod/expected/com/example/SyntheticFunction4.java b/src/test/resources/unusedfuncinterfacemethod/expected/com/example/SyntheticFunction4.java index 684d580d2..1b554ccd4 100644 --- a/src/test/resources/unusedfuncinterfacemethod/expected/com/example/SyntheticFunction4.java +++ b/src/test/resources/unusedfuncinterfacemethod/expected/com/example/SyntheticFunction4.java @@ -1,5 +1,6 @@ package com.example; +@FunctionalInterface public interface SyntheticFunction4 { public T4 apply(T parameter0, T1 parameter1, T2 parameter2, T3 parameter3); diff --git a/src/test/resources/unusedimports/expected/com/example/ManyImports.java b/src/test/resources/unusedimports/expected/com/example/ManyImports.java index 9aa3a03aa..26708b1db 100644 --- a/src/test/resources/unusedimports/expected/com/example/ManyImports.java +++ b/src/test/resources/unusedimports/expected/com/example/ManyImports.java @@ -1,4 +1,8 @@ package com.example; +import static java.util.Map.*; + +import java.io.*; + public class ManyImports { } diff --git a/src/test/resources/unusedimports/expected/com/example/Simple.java b/src/test/resources/unusedimports/expected/com/example/Simple.java index c0b859517..a62d3af94 100644 --- a/src/test/resources/unusedimports/expected/com/example/Simple.java +++ b/src/test/resources/unusedimports/expected/com/example/Simple.java @@ -1,10 +1,11 @@ package com.example; -import java.util.List; -import java.io.*; -import static java.util.Map.*; -import static java.lang.Math.sqrt; import static java.lang.Math.PI; +import static java.lang.Math.sqrt; +import static java.util.Map.*; + +import java.io.*; +import java.util.List; public class Simple { diff --git a/src/test/resources/unusedtypeparameterbound2/expected/org/testing/Baz.java b/src/test/resources/unusedtypeparameterbound2/expected/org/testing/Baz.java index 894081186..d8b6f0499 100644 --- a/src/test/resources/unusedtypeparameterbound2/expected/org/testing/Baz.java +++ b/src/test/resources/unusedtypeparameterbound2/expected/org/testing/Baz.java @@ -2,7 +2,7 @@ public class Baz { - public BazMethodReturnType bazMethod() { + public org.testing.BazMethodReturnType bazMethod() { throw new java.lang.Error(); } } diff --git a/src/test/resources/voidreturndouble/expected/com/example/Simple.java b/src/test/resources/voidreturndouble/expected/com/example/Simple.java index f5bc85a0d..bae5dcfa9 100644 --- a/src/test/resources/voidreturndouble/expected/com/example/Simple.java +++ b/src/test/resources/voidreturndouble/expected/com/example/Simple.java @@ -1,7 +1,7 @@ package com.example; -import org.example.MethodGen; import org.example.Foo; +import org.example.MethodGen; public class Simple { diff --git a/src/test/resources/wildcardimport/expected/org/example/Foo.java b/src/test/resources/wildcardimport/expected/org/example/Foo.java index 194b92ac0..9d795929b 100644 --- a/src/test/resources/wildcardimport/expected/org/example/Foo.java +++ b/src/test/resources/wildcardimport/expected/org/example/Foo.java @@ -2,11 +2,11 @@ public class Foo { - public Foo() { + public org.example.FooMethodReturnType fooMethod() { throw new java.lang.Error(); } - public FooMethodReturnType fooMethod() { + public Foo() { throw new java.lang.Error(); } } diff --git a/src/test/resources/wildcardimport2/expected/org/example/Foo.java b/src/test/resources/wildcardimport2/expected/org/example/Foo.java index 194b92ac0..9d795929b 100644 --- a/src/test/resources/wildcardimport2/expected/org/example/Foo.java +++ b/src/test/resources/wildcardimport2/expected/org/example/Foo.java @@ -2,11 +2,11 @@ public class Foo { - public Foo() { + public org.example.FooMethodReturnType fooMethod() { throw new java.lang.Error(); } - public FooMethodReturnType fooMethod() { + public Foo() { throw new java.lang.Error(); } } diff --git a/typecheck_one_test.bat b/typecheck_one_test.bat index 8346beb2e..27b6d5a10 100644 --- a/typecheck_one_test.bat +++ b/typecheck_one_test.bat @@ -13,11 +13,6 @@ set testcase=%1 if "%testcase%"=="shared" exit /b 0 rem https://bugs.openjdk.org/browse/JDK-8319461 wasn't actually fixed (this test is based on that bug) if "%testcase%"=="superinterfaceextends" exit /b 0 -rem incomplete handling of method references: https://github.com/njit-jerse/specimin/issues/291 -rem this test exists to check that no crash occurs, not that Specimin produces the correct output -if "%testcase%"=="methodref2" exit /b 0 -rem this test will not compile right now; this is a TODO in UnsolvedSymbolVisitor#lookupTypeArgumentFQN -if "%testcase%"=="methodreturnfullyqualifiedgeneric" exit /b 0 cd "%testcase%\expected\" || exit /b 2 @@ -26,6 +21,7 @@ for /r %%F in (*.java) do ( set "JAVA_FILES=!JAVA_FILES! %%F" ) +set returnval=0 javac -classpath "../../shared/checker-qual-3.42.0.jar" !JAVA_FILES! if errorlevel 1 ( echo Running javac on %testcase% resulted in one or more errors, which are printed above. @@ -36,6 +32,6 @@ rem clean up for /r %%F in (*.class) do ( del "%%F" ) -endlocal -exit /b 0 \ No newline at end of file +exit /b !returnval! +endlocal \ No newline at end of file diff --git a/typecheck_one_test.sh b/typecheck_one_test.sh index e88685149..5094593fe 100644 --- a/typecheck_one_test.sh +++ b/typecheck_one_test.sh @@ -11,11 +11,6 @@ testcase=$1 if [ "${testcase}" = "shared" ]; then exit 0; fi # https://bugs.openjdk.org/browse/JDK-8319461 wasn't actually fixed (this test is based on that bug) if [ "${testcase}" = "superinterfaceextends" ]; then exit 0; fi -# incomplete handling of method references: https://github.com/njit-jerse/specimin/issues/291 -# this test exists to check that no crash occurs, not that Specimin produces the correct output -if [ "${testcase}" = "methodref2" ]; then exit 0; fi -# this test will not compile right now; this is a TODO in UnsolvedSymbolVisitor#lookupTypeArgumentFQN -if [ "${testcase}" = "methodreturnfullyqualifiedgeneric" ]; then exit 0; fi cd "${testcase}/expected/" || exit 2 # javac relies on word splitting # shellcheck disable=SC2046 diff --git a/typecheck_test_outputs.bat b/typecheck_test_outputs.bat index 426bc1e89..84e6b07e1 100644 --- a/typecheck_test_outputs.bat +++ b/typecheck_test_outputs.bat @@ -29,5 +29,5 @@ if !returnval!==0 ( ) ) -endlocal -exit /b !returnval! \ No newline at end of file +exit /b !returnval! +endlocal \ No newline at end of file