diff --git a/.gitignore b/.gitignore index af2744faa7..9c422eb667 100644 --- a/.gitignore +++ b/.gitignore @@ -62,10 +62,10 @@ manual.dvi manual.haux manual.html manual.htoc +manual.image.log manual.image.out manual.image.tex manual.image.tex.new -manual.image.log manual.log manual.out manual.pdf @@ -79,20 +79,20 @@ manual003.gif manual003.png manual004.gif manual004.png -docs/manual/contributors.txt -docs/manual/figures/*.eps -docs/manual/figures/*.pdf -docs/manual/figures/*.png -docs/manual/figures/signature-types-with-canonicalname.dia -docs/manual/*.png -docs/manual/*.svg docs/examples/**/*.class docs/examples/MavenExample/Out.txt docs/examples/errorprone/.gradle/ docs/examples/errorprone/Out.txt -docs/examples/lombok/Out.txt docs/examples/lombok/.gradle/ +docs/examples/lombok/Out.txt docs/examples/lombok/lombok.config +docs/manual/*.png +docs/manual/*.svg +docs/manual/contributors.txt +docs/manual/figures/*.eps +docs/manual/figures/*.pdf +docs/manual/figures/*.png +docs/manual/figures/signature-types-with-canonicalname.dia docs/manual/manual.html-e docs/tmpapi/ @@ -125,21 +125,23 @@ checker/tests/nullness-extra/*.class checker/tests/nullness-extra/compat/Out.txt checker/tests/nullness-extra/compat/javax/annotation/Nullable.class checker/tests/nullness-extra/compat/lib/Lib.class -checker/tests/nullness-extra/multiple-errors/*.class -checker/tests/nullness-extra/multiple-errors/Out.txt -checker/tests/nullness-extra/package-anno/Out.txt -checker/tests/nullness-extra/package-anno/test/*.class -checker/tests/nullness-extra/shorthand/NullnessRegexWithErrors.class -checker/tests/nullness-extra/shorthand/Out.txt +checker/tests/nullness-extra/issue3597/testpkg/Issue3597A.class +checker/tests/nullness-extra/issue3597/testpkg/Issue3597B.class checker/tests/nullness-extra/issue502/Issue502.class checker/tests/nullness-extra/issue502/Out.txt checker/tests/nullness-extra/issue594/Out.txt checker/tests/nullness-extra/issue607/Issue607.class checker/tests/nullness-extra/issue607/Issue607Interface.class checker/tests/nullness-extra/issue607/Issue607SuperClass.class -checker/tests/nullness/generics/*.class -checker/tests/nullness-temp/*.java +checker/tests/nullness-extra/multiple-errors/*.class +checker/tests/nullness-extra/multiple-errors/Out.txt +checker/tests/nullness-extra/package-anno/Out.txt +checker/tests/nullness-extra/package-anno/test/*.class +checker/tests/nullness-extra/shorthand/NullnessRegexWithErrors.class +checker/tests/nullness-extra/shorthand/Out.txt checker/tests/nullness-temp/*.class +checker/tests/nullness-temp/*.java +checker/tests/nullness/generics/*.class dataflow/tests/issue3447/Out.txt dataflow/tests/issue3447/*.class @@ -152,8 +154,8 @@ checker/jtreg/multipleexecutions/Main.class checker-qual-android/src # Some tests produce output into the tests/ directory instead of the build/ directory. -checker/tests/ainfer-testchecker/annotated/ -checker/tests/ainfer-testchecker/inference-output/ checker/tests/ainfer-nullness/annotated/ checker/tests/ainfer-nullness/inference-output/ +checker/tests/ainfer-testchecker/annotated/ +checker/tests/ainfer-testchecker/inference-output/ framework/tests/returnsreceiverdelomboked/ diff --git a/SKIP-REQUIRE-JAVADOC b/SKIP-REQUIRE-JAVADOC deleted file mode 100644 index 1a80130490..0000000000 --- a/SKIP-REQUIRE-JAVADOC +++ /dev/null @@ -1 +0,0 @@ -Delete this file after the pull request is merged. diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 371b4edddf..471dffb22a 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -34,6 +34,21 @@ jobs: fetchDepth: 25 - bash: ./checker/bin-devel/test-cftests-junit.sh displayName: test-cftests-junit.sh +- job: junit_tests_jdk17 + dependsOn: + - junit_tests_jdk11 + - nonjunit_tests_jdk11 + - misc_jdk11 + - typecheck_jdk11 + pool: + vmImage: 'ubuntu-latest' + container: mdernst/cf-ubuntu-jdk17:latest + timeoutInMinutes: 70 + steps: + - checkout: self + fetchDepth: 25 + - bash: ./checker/bin-devel/test-cftests-junit.sh + displayName: test-cftests-junit.sh - job: nonjunit_tests_jdk8 dependsOn: - junit_tests_jdk11 @@ -57,6 +72,20 @@ jobs: fetchDepth: 25 - bash: ./checker/bin-devel/test-cftests-nonjunit.sh displayName: test-cftests-nonjunit.sh +- job: nonjunit_tests_jdk17 + dependsOn: + - junit_tests_jdk11 + - nonjunit_tests_jdk11 + - misc_jdk11 + - typecheck_jdk11 + pool: + vmImage: 'ubuntu-latest' + container: mdernst/cf-ubuntu-jdk17:latest + steps: + - checkout: self + fetchDepth: 25 + - bash: ./checker/bin-devel/test-cftests-nonjunit.sh + displayName: test-cftests-nonjunit.sh - job: inference_tests_jdk8 dependsOn: - junit_tests_jdk11 @@ -80,13 +109,26 @@ jobs: fetchDepth: 25 - bash: ./checker/bin-devel/test-cftests-inference.sh displayName: test-cftests-inference.sh +- job: inference_tests_jdk17 + dependsOn: + - junit_tests_jdk11 + - inference_tests_jdk11 + - misc_jdk11 + - typecheck_jdk11 + pool: + vmImage: 'ubuntu-latest' + container: mdernst/cf-ubuntu-jdk17:latest + steps: + - checkout: self + fetchDepth: 25 + - bash: ./checker/bin-devel/test-cftests-inference.sh + displayName: test-cftests-inference.sh - job: misc_jdk8 - ## The dependsOn is commented out because misc_jdk8 sometimes fails when misc_jdk11 does not. - # dependsOn: - # - junit_tests_jdk11 - # - nonjunit_tests_jdk11 - # - misc_jdk11 - # - typecheck_jdk11 + dependsOn: + - junit_tests_jdk11 + - nonjunit_tests_jdk11 + - misc_jdk11 + - typecheck_jdk11 pool: vmImage: 'ubuntu-latest' container: mdernst/cf-ubuntu-jdk8-plus:latest @@ -104,6 +146,20 @@ jobs: fetchDepth: 1000 - bash: ./checker/bin-devel/test-misc.sh displayName: test-misc.sh +- job: misc_jdk17 + dependsOn: + - junit_tests_jdk11 + - nonjunit_tests_jdk11 + - misc_jdk11 + - typecheck_jdk11 + pool: + vmImage: 'ubuntu-latest' + container: mdernst/cf-ubuntu-jdk17-plus:latest + steps: + - checkout: self + fetchDepth: 1000 + - bash: ./checker/bin-devel/test-misc.sh + displayName: test-misc.sh - job: typecheck_jdk8 dependsOn: - junit_tests_jdk11 @@ -127,6 +183,20 @@ jobs: fetchDepth: 1000 - bash: ./checker/bin-devel/test-typecheck.sh displayName: test-typecheck.sh +- job: typecheck_jdk17 + dependsOn: + - junit_tests_jdk11 + - nonjunit_tests_jdk11 + - misc_jdk11 + - typecheck_jdk11 + pool: + vmImage: 'ubuntu-latest' + container: mdernst/cf-ubuntu-jdk17-plus:latest + steps: + - checkout: self + fetchDepth: 1000 + - bash: ./checker/bin-devel/test-typecheck.sh + displayName: test-typecheck.sh - job: daikon_jdk8 dependsOn: - junit_tests_jdk11 @@ -154,6 +224,24 @@ jobs: fetchDepth: 25 - bash: ./checker/bin-devel/test-daikon.sh displayName: test-daikon.sh +## Daikon does not yet support JDK 17. TODO: Make Daikon run under JDK 17. +# - job: daikon_jdk17 +# dependsOn: +# - junit_tests_jdk11 +# - nonjunit_tests_jdk11 +# - misc_jdk11 +# - typecheck_jdk11 +# # ## Commented to reduce latency and eliminate the "daikon_jdk11 -> daikon_jdk17" critical path. +# # # - daikon_jdk11 +# pool: +# vmImage: 'ubuntu-latest' +# container: mdernst/cf-ubuntu-jdk17:latest +# timeoutInMinutes: 70 +# steps: +# - checkout: self +# fetchDepth: 25 +# - bash: ./checker/bin-devel/test-daikon.sh +# displayName: test-daikon.sh - job: guava_jdk8 dependsOn: - junit_tests_jdk11 @@ -178,6 +266,21 @@ jobs: fetchDepth: 25 - bash: ./checker/bin-devel/test-guava.sh displayName: test-guava.sh +- job: guava_jdk17 + dependsOn: + - junit_tests_jdk11 + - nonjunit_tests_jdk11 + - misc_jdk11 + - typecheck_jdk11 + - guava_jdk11 + pool: + vmImage: 'ubuntu-latest' + container: mdernst/cf-ubuntu-jdk17:latest + steps: + - checkout: self + fetchDepth: 25 + - bash: ./checker/bin-devel/test-guava.sh + displayName: test-guava.sh - job: plume_lib_jdk8 dependsOn: - junit_tests_jdk11 @@ -202,6 +305,22 @@ jobs: fetchDepth: 25 - bash: ./checker/bin-devel/test-plume-lib.sh displayName: test-plume-lib.sh +- job: plume_lib_jdk17 + dependsOn: + - junit_tests_jdk11 + - nonjunit_tests_jdk11 + - misc_jdk11 + - typecheck_jdk11 + - plume_lib_jdk11 + pool: + vmImage: 'ubuntu-latest' + container: mdernst/cf-ubuntu-jdk17:latest + steps: + - checkout: self + fetchDepth: 25 + - bash: ./checker/bin-devel/test-plume-lib.sh + displayName: test-plume-lib.sh +## The downstream jobs are not currently needed because test-downstream.sh is empty. # - job: downstream_jdk8 # dependsOn: # - junit_tests_jdk11 @@ -226,3 +345,18 @@ jobs: # fetchDepth: 25 # - bash: ./checker/bin-devel/test-downstream.sh # displayName: test-downstream.sh +# - job: downstream_jdk17 +# dependsOn: +# - junit_tests_jdk11 +# - nonjunit_tests_jdk11 +# - misc_jdk11 +# - typecheck_jdk11 +# - downstream_jdk11 +# pool: +# vmImage: 'ubuntu-latest' +# container: mdernst/cf-ubuntu-jdk17:latest +# steps: +# - checkout: self +# fetchDepth: 25 +# - bash: ./checker/bin-devel/test-downstream.sh +# displayName: test-downstream.sh diff --git a/build.gradle b/build.gradle index bc958661ac..a80e88434f 100644 --- a/build.gradle +++ b/build.gradle @@ -2,14 +2,14 @@ import de.undercouch.gradle.tasks.download.Download plugins { // https://plugins.gradle.org/plugin/com.github.johnrengelman.shadow (v5 requires Gradle 5) - id 'com.github.johnrengelman.shadow' version '6.1.0' + id 'com.github.johnrengelman.shadow' version '7.1.2' // https://plugins.gradle.org/plugin/de.undercouch.download id "de.undercouch.download" version "4.1.2" id 'java' // https://github.com/tbroyer/gradle-errorprone-plugin id "net.ltgt.errorprone" version "2.0.2" // https://plugins.gradle.org/plugin/org.ajoberstar.grgit - id 'org.ajoberstar.grgit' version '4.1.0' apply false + id 'org.ajoberstar.grgit' version '4.1.1' apply false } apply plugin: "de.undercouch.download" @@ -26,6 +26,8 @@ ext { // On a Java 9+ JVM, use the host javac, default source/target, and required module flags. isJava8 = JavaVersion.current() == JavaVersion.VERSION_1_8 + isJava17 = JavaVersion.current() == JavaVersion.VERSION_17 + errorproneJavacVersion = '9+181-r4173-1' parentDir = file("${rootDir}/../").absolutePath @@ -34,7 +36,7 @@ ext { afu = "${annotationTools}/annotation-file-utilities" stubparser = "${parentDir}/stubparser" - stubparserJar = "${stubparser}/javaparser-core/target/stubparser-3.22.1.jar" + stubparserJar = "${stubparser}/javaparser-core/target/stubparser-3.23.1.jar" jtregHome = "${parentDir}/jtreg" formatScriptsHome = "${project(':checker').projectDir}/bin-devel/.run-google-java-format" @@ -53,14 +55,15 @@ switch (JavaVersion.current()) { case JavaVersion.VERSION_1_9: case JavaVersion.VERSION_1_10: case JavaVersion.VERSION_12: - logger.warn("The Checker Framework has only been tested with JDK 8 and 11." + + logger.warn("The Checker Framework has only been tested with JDK 8, 11, and 17." + " Found version " + JavaVersion.current().majorVersion); break; case JavaVersion.VERSION_1_8: case JavaVersion.VERSION_11: + case JavaVersion.VERSION_17: break; // Supported versions default: - throw new GradleException("Build the Checker Framework with JDK 8 or JDK 11." + + throw new GradleException("Build the Checker Framework with JDK 8, 11, or 17." + " Found version " + JavaVersion.current().majorVersion); } @@ -92,7 +95,7 @@ allprojects { // level (third number) if: // * any new checkers have been added, or // * backward-incompatible changes have been made to APIs or elsewhere. - version '3.15.1-SNAPSHOT' + version '3.21.1-SNAPSHOT' repositories { mavenCentral() @@ -118,6 +121,28 @@ allprojects { allProjects subprojects } + ext { + // A list of add-export and add-open arguments to be used when running the Checker Framework. + // Keep this list in sync with the lists in CheckerMain#getExecArguments, + // the sections with labels "javac-jdk11-non-modularized", "maven", and "sbt" in the manual + // and in the checker-framework-gradle-plugin, CheckerFrameworkPlugin#applyToProject + compilerArgsForRunningCF = [ + // These are required in Java 16+ because the --illegal-access option is set to deny + // by default. None of these packages are accessed via reflection, so the module + // only needs to be exported, but not opened. + "--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.main=ALL-UNNAMED", + "--add-exports", "jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED", + "--add-exports", "jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED", + "--add-exports", "jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED", + "--add-exports", "jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED", + // Required because the Checker Framework reflectively accesses private members in com.sun.tools.javac.comp. + "--add-opens", "jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED", + ] + } + // After all the tasks have been created, modify some of them. afterEvaluate { @@ -213,7 +238,8 @@ allprojects { // -options: To not get a warning about missing bootstrap classpath for Java 8 (once we use Java 9). // -fallthrough: Don't check fallthroughs. Instead, use Error Prone. Its // warnings are suppressible with a "// fall through" comment. - "-Xlint:-options,-fallthrough", + // -classfile: classgraph jar file and https://bugs.openjdk.java.net/browse/JDK-8190452 + "-Xlint:-options,-fallthrough,-classfile", "-Xlint", ] @@ -353,9 +379,7 @@ def createCheckTypeTask(projectName, taskName, checker, args = []) { ] } else { options.fork = true - options.forkOptions.jvmArgs += [ - "--add-opens", "jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED", - ] + options.forkOptions.jvmArgs += compilerArgsForRunningCF } } } @@ -379,8 +403,11 @@ List getJavaFilesToFormat(projectName) { fileTree("${project(projectName).projectDir}/tests").visit { details -> // If you change this, also change checker/bin-devel/git.pre-commit if (!details.path.contains("nullness-javac-errors") + && !details.path.contains("calledmethods-delomboked") && !details.path.contains("returnsreceiverdelomboked") && !details.path.contains("build") + && (isJava17 || !details.path.contains("-records")) + && (isJava17 || !details.path.contains("java17")) && details.name.endsWith('.java')) { javaFiles.add(details.file) } @@ -407,6 +434,24 @@ List getJavaFilesToFormat(projectName) { return args } +task writeListOfJavaFilesToFormat(group: 'Format') { + description "Writes a list of Java files subject to formatting, to ${buildDir}/javafiles.txt" + doLast { + mkdir buildDir + File list = new File("${buildDir}/javafiles.txt"); + list.createNewFile(); + FileWriter fileWriter = new FileWriter(list) + subprojects.forEach { + if (!it.name.startsWith("checker-qual-android")) { + for (String fileName : getJavaFilesToFormat(it.name)) { + fileWriter.write(fileName) + fileWriter.write(System.lineSeparator()) + } + } + } + } +} + task htmlValidate(type: Exec, group: 'Format') { description 'Validate that HTML files are well-formed' executable 'html5validator' @@ -636,6 +681,7 @@ task tags { subprojects { configurations { errorprone + annotatedGuava } dependencies { @@ -644,9 +690,15 @@ subprojects { // * Temporarily comment out "-Werror" elsewhere in this file // * Repeatedly run `./gradlew clean compileJava` and fix all errors // * Uncomment "-Werror" - // * Don't edit framework/build.gradle to use the same version number - // (it is updated when the annotated version of Guava is updated). - errorprone group: 'com.google.errorprone', name: 'error_prone_core', version: '2.7.1' + ext.errorproneVersion = '2.10.0' + errorprone group: 'com.google.errorprone', name: 'error_prone_core', version: errorproneVersion + + // TODO: it's a bug that annotatedlib:guava requires the error_prone_annotations dependency. + annotatedGuava "com.google.errorprone:error_prone_annotations:${errorproneVersion}" + annotatedGuava ('org.checkerframework.annotatedlib:guava:30.1.1-jre') { + // So long as Guava only uses annotations from checker-qual, excluding it should not cause problems. + exclude group: 'org.checkerframework' + } } task checkFormat(type: Exec, dependsOn: [getCodeFormatScripts, pythonIsInstalled], group: 'Format') { @@ -662,6 +714,9 @@ subprojects { } args += "${formatScriptsHome}/check-google-java-format.py" args += getJavaFilesToFormat(project.name) + // Since the scripts are downloaded from third-party Github, the environment variable + // is the only way to add the necessary open: + environment('JDK_JAVA_OPTIONS', '--add-opens jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED --add-opens jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED --add-opens jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED --add-opens jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED --add-opens jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED --add-opens jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED --add-opens jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED --add-opens jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED') } ignoreExitValue = true doLast { @@ -819,7 +874,16 @@ subprojects { // uses the jdk.jdeps module. "-javacoptions:--add-modules jdk.jdeps", "-javacoptions:--add-exports=jdk.jdeps/com.sun.tools.classfile=ALL-UNNAMED", + "-vmoptions:--add-opens=jdk.jdeps/com.sun.tools.classfile=ALL-UNNAMED", + "-vmoptions:--add-opens=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED", + "-vmoptions:--add-opens=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED", "-vmoptions:--add-opens=jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED", + "-vmoptions:--add-opens=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED", + "-vmoptions:--add-opens=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED", + "-vmoptions:--add-opens=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED", + "-vmoptions:--add-opens=jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED", + "-vmoptions:--add-opens=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED", + "-vmoptions:--add-opens=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED", ] } if (project.name.is('framework')) { @@ -854,10 +918,7 @@ subprojects { if (isJava8) { jvmArgs "-Xbootclasspath/p:${configurations.javacJar.asPath}".toString() } else { - jvmArgs += [ - "--illegal-access=warn", - "--add-opens", "jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED", - ] + jvmArgs += compilerArgsForRunningCF } maxParallelForks = Integer.MAX_VALUE diff --git a/checker-qual/build.gradle b/checker-qual/build.gradle index f5cd3e3ac5..c17cb4271c 100644 --- a/checker-qual/build.gradle +++ b/checker-qual/build.gradle @@ -4,7 +4,7 @@ buildscript { } dependencies { // Create OSGI bundles - classpath "biz.aQute.bnd:biz.aQute.bnd.gradle:5.3.0" + classpath "biz.aQute.bnd:biz.aQute.bnd.gradle:6.1.0" // Don't add implementation dependencies; checker-qual.jar should have no dependencies. } } diff --git a/checker-qual/src/main/java/org/checkerframework/checker/mustcall/qual/NotOwning.java b/checker-qual/src/main/java/org/checkerframework/checker/mustcall/qual/NotOwning.java index 66ecd11a10..bea1229b96 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/mustcall/qual/NotOwning.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/mustcall/qual/NotOwning.java @@ -7,7 +7,8 @@ /** * Annotation indicating that ownership should not be transferred to the annotated parameter, field, - * or (when this is written on a method) return type, for the purposes of Must Call checking. + * or method's call sites, for the purposes of Must Call checking. For a full description of the + * semantics, see the documentation of {@link Owning}. * *

Parameters and fields are treated as if they have this annotation by default unless they have * {@link Owning}. diff --git a/checker-qual/src/main/java/org/checkerframework/checker/mustcall/qual/Owning.java b/checker-qual/src/main/java/org/checkerframework/checker/mustcall/qual/Owning.java index f6f0c75471..caed657d79 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/mustcall/qual/Owning.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/mustcall/qual/Owning.java @@ -6,12 +6,26 @@ import java.lang.annotation.Target; /** - * Annotation indicating that ownership should be transferred to the annotated parameter, field, or - * (when written on a method) return type, for the purposes of Must Call checking. Static fields - * cannot be owning. + * Annotation indicating that ownership should be transferred to the annotated element for the + * purposes of Must Call checking. When written on a parameter, the annotation indicates that Must + * Call checking should be performed in the body of the method, not at call sites. When written on a + * method, the annotation indicates that return expressions do not need to be checked in the method + * body, but at call sites. When written on a field, the annotation indicates that fulfilling the + * must-call obligations of an instance of the class in which the field is declared also results in + * the annotated field's must-call obligations being satisfied. Static fields cannot be owning. * - *

Method return types are treated as if they have this annotation by default unless their method - * is annotated as {@link NotOwning}. + *

This annotation is a declaration annotation rather than a type annotation, because it does not + * logically belong to any type hierarchy. Logically, it is a directive to the Resource Leak Checker + * that informs it whether it is necessary to check that a value's must-call obligations have been + * satisfied. In that way, it can be viewed as an annotation expressing an aliasing relationship: + * passing a object with a non-empty must-call obligation to a method with an owning parameter + * resolves that object's must-call obligation, because the ownership annotation expresses that the + * object at the call site and the parameter in the method's body are aliases, and so checking only + * one of the two is required. + * + *

Methods are treated as if they have this annotation by default unless their method is + * annotated as {@link NotOwning}. Parameters and fields are treated as {@link NotOwning} by + * default. * *

When the -AnoLightweightOwnership command-line argument is passed to the checker, this * annotation and {@link NotOwning} are ignored. diff --git a/checker-qual/src/main/java/org/checkerframework/checker/nullness/qual/RequiresNonNull.java b/checker-qual/src/main/java/org/checkerframework/checker/nullness/qual/RequiresNonNull.java index 39e7b13430..1c32fab5a8 100644 --- a/checker-qual/src/main/java/org/checkerframework/checker/nullness/qual/RequiresNonNull.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/nullness/qual/RequiresNonNull.java @@ -2,6 +2,7 @@ import java.lang.annotation.Documented; import java.lang.annotation.ElementType; +import java.lang.annotation.Repeatable; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @@ -61,13 +62,35 @@ @Documented @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD, ElementType.CONSTRUCTOR}) +@Repeatable(RequiresNonNull.List.class) @PreconditionAnnotation(qualifier = NonNull.class) public @interface RequiresNonNull { /** * The Java expressions that need to be {@link * org.checkerframework.checker.nullness.qual.NonNull}. * + * @return the Java expressions that need to be {@link + * org.checkerframework.checker.nullness.qual.NonNull} * @checker_framework.manual #java-expressions-as-arguments Syntax of Java expressions */ String[] value(); + + /** + * A wrapper annotation that makes the {@link RequiresNonNull} annotation repeatable. + * + *

Programmers generally do not need to write this. It is created by Java when a programmer + * writes more than one {@link RequiresNonNull} annotation at the same location. + */ + @Documented + @Retention(RetentionPolicy.RUNTIME) + @Target({ElementType.METHOD, ElementType.CONSTRUCTOR}) + @PreconditionAnnotation(qualifier = NonNull.class) + @interface List { + /** + * Returns the repeatable annotations. + * + * @return the repeatable annotations + */ + RequiresNonNull[] value(); + } } diff --git a/checker-qual/src/main/java/org/checkerframework/dataflow/qual/TerminatesExecution.java b/checker-qual/src/main/java/org/checkerframework/dataflow/qual/TerminatesExecution.java index 412c4be48e..71d4e87695 100644 --- a/checker-qual/src/main/java/org/checkerframework/dataflow/qual/TerminatesExecution.java +++ b/checker-qual/src/main/java/org/checkerframework/dataflow/qual/TerminatesExecution.java @@ -28,6 +28,17 @@ *

This annotation is inherited by subtypes, just as if it were meta-annotated with * {@code @InheritedAnnotation}. * + *

The Checker Framework recognizes this annotation, but the Java compiler {@code javac} does + * not. After calling a method annotated with {@code TerminatesExecution}, to prevent a {@code + * javac} diagnostic, you generally need to insert a {@code throw} statement (which you know will + * never execute): + * + *

+ * ...
+ * myTerminatingMethod();
+ * throw new Error("unreachable");
+ * 
+ * * @checker_framework.manual #type-refinement Automatic type refinement (flow-sensitive type * qualifier inference) */ diff --git a/checker-util/src/main/java/org/checkerframework/checker/i18nformatter/util/I18nFormatUtil.java b/checker-util/src/main/java/org/checkerframework/checker/i18nformatter/util/I18nFormatUtil.java index c47f42c154..499084e2af 100644 --- a/checker-util/src/main/java/org/checkerframework/checker/i18nformatter/util/I18nFormatUtil.java +++ b/checker-util/src/main/java/org/checkerframework/checker/i18nformatter/util/I18nFormatUtil.java @@ -300,7 +300,7 @@ private static void makeFormat(int offsetNumber, @Nullable StringBuilder[] textS argumentIndices.add(argumentNumber); // now get the format - I18nConversionCategory category = null; + final I18nConversionCategory category; if (segments[SEG_TYPE].length() != 0) { int type = findKeyword(segments[SEG_TYPE], TYPE_KEYWORDS); switch (type) { diff --git a/checker/bin-devel/Dockerfile-README b/checker/bin-devel/Dockerfile-README index d9fb09df47..66405ec93e 100644 --- a/checker/bin-devel/Dockerfile-README +++ b/checker/bin-devel/Dockerfile-README @@ -49,12 +49,12 @@ export PROJECT=cf create_upload_docker_image export OS=ubuntu -export JDKVER=jdk16 +export JDKVER=jdk17 export PROJECT=cf create_upload_docker_image export OS=ubuntu -export JDKVER=jdk16-plus +export JDKVER=jdk17-plus export PROJECT=cf create_upload_docker_image diff --git a/checker/bin-devel/Dockerfile-ubuntu-jdk16 b/checker/bin-devel/Dockerfile-ubuntu-jdk17 similarity index 64% rename from checker/bin-devel/Dockerfile-ubuntu-jdk16 rename to checker/bin-devel/Dockerfile-ubuntu-jdk17 index 658bfd117f..7728b3fc54 100644 --- a/checker/bin-devel/Dockerfile-ubuntu-jdk16 +++ b/checker/bin-devel/Dockerfile-ubuntu-jdk17 @@ -1,5 +1,5 @@ # Create a Docker image that is ready to run the main Checker Framework tests, -# using JDK 16. +# using JDK 17. # "ubuntu" is the latest LTS release. "ubuntu:rolling" is the latest release. FROM ubuntu @@ -10,8 +10,8 @@ MAINTAINER Michael Ernst # * Put "apt-get update" and "apt-get install" and apt cleanup in the same RUN command. # * Do not run "apt-get upgrade"; instead get upstream to update. -# Instructions for installing Java 16 on Ubuntu: -# https://www.linuxuprising.com/2021/03/how-to-install-oracle-java-16-on-debian.html +# Instructions for installing Java 17 on Ubuntu: +# https://www.linuxuprising.com/2021/09/how-to-install-oracle-java-17-lts-on.html RUN export DEBIAN_FRONTEND=noninteractive \ && apt-get -qqy update \ && apt-get -qqy install software-properties-common \ @@ -19,9 +19,10 @@ RUN export DEBIAN_FRONTEND=noninteractive \ RUN export DEBIAN_FRONTEND=noninteractive \ && apt-get -qqy update \ -&& echo oracle-java16-installer shared/accepted-oracle-license-v1-2 select true | /usr/bin/debconf-set-selections \ +&& echo oracle-java17-installer shared/accepted-oracle-license-v1-3 select true | /usr/bin/debconf-set-selections \ +&& echo oracle-java17-installer shared/accepted-oracle-licence-v1-3 boolean true | /usr/bin/debconf-set-selections \ && apt-get -qqy install \ - oracle-java16-installer --install-recommends + oracle-java17-installer --install-recommends RUN export DEBIAN_FRONTEND=noninteractive \ && apt-get -qqy update \ @@ -41,9 +42,9 @@ RUN export DEBIAN_FRONTEND=noninteractive \ wget RUN export DEBIAN_FRONTEND=noninteractive \ -&& wget https://mirrors.sonic.net/apache/maven/maven-3/3.8.1/binaries/apache-maven-3.8.1-bin.tar.gz \ -&& tar xzvf apache-maven-3.8.1-bin.tar.gz -ENV PATH="/apache-maven-3.8.1/bin:$PATH" +&& wget https://mirrors.sonic.net/apache/maven/maven-3/3.8.3/binaries/apache-maven-3.8.3-bin.tar.gz \ +&& tar xzvf apache-maven-3.8.3-bin.tar.gz +ENV PATH="/apache-maven-3.8.3/bin:$PATH" RUN export DEBIAN_FRONTEND=noninteractive \ && apt-get clean \ diff --git a/checker/bin-devel/Dockerfile-ubuntu-jdk16-plus b/checker/bin-devel/Dockerfile-ubuntu-jdk17-plus similarity index 70% rename from checker/bin-devel/Dockerfile-ubuntu-jdk16-plus rename to checker/bin-devel/Dockerfile-ubuntu-jdk17-plus index 8a08092d4e..1cd43349b1 100644 --- a/checker/bin-devel/Dockerfile-ubuntu-jdk16-plus +++ b/checker/bin-devel/Dockerfile-ubuntu-jdk17-plus @@ -1,5 +1,5 @@ # Create a Docker image that is ready to run the full Checker Framework tests, -# including building the manual and Javadoc, using JDK 16. +# including building the manual and Javadoc, using JDK 17. # "ubuntu" is the latest LTS release. "ubuntu:rolling" is the latest release. FROM ubuntu @@ -12,8 +12,8 @@ MAINTAINER Michael Ernst # * Put "apt-get update" and "apt-get install" and apt cleanup in the same RUN command. # * Do not run "apt-get upgrade"; instead get upstream to update. -# Instructions for installing Java 16 on Ubuntu: -# https://www.linuxuprising.com/2021/03/how-to-install-oracle-java-16-on-debian.html +# Instructions for installing Java 17 on Ubuntu: +# https://www.linuxuprising.com/2021/09/how-to-install-oracle-java-17-lts-on.html RUN export DEBIAN_FRONTEND=noninteractive \ && apt-get -qqy update \ && apt-get -qqy install software-properties-common \ @@ -21,9 +21,10 @@ RUN export DEBIAN_FRONTEND=noninteractive \ RUN export DEBIAN_FRONTEND=noninteractive \ && apt-get -qqy update \ -&& echo oracle-java16-installer shared/accepted-oracle-license-v1-2 select true | /usr/bin/debconf-set-selections \ +&& echo oracle-java17-installer shared/accepted-oracle-license-v1-3 select true | /usr/bin/debconf-set-selections \ +&& echo oracle-java17-installer shared/accepted-oracle-licence-v1-3 boolean true | /usr/bin/debconf-set-selections \ && apt-get -qqy install \ - oracle-java16-installer --install-recommends + oracle-java17-installer --install-recommends RUN export DEBIAN_FRONTEND=noninteractive \ && apt-get -qqy update \ @@ -43,9 +44,9 @@ RUN export DEBIAN_FRONTEND=noninteractive \ wget RUN export DEBIAN_FRONTEND=noninteractive \ -&& wget https://mirrors.sonic.net/apache/maven/maven-3/3.8.1/binaries/apache-maven-3.8.1-bin.tar.gz \ -&& tar xzvf apache-maven-3.8.1-bin.tar.gz -ENV PATH="/apache-maven-3.8.1/bin:$PATH" +&& wget https://mirrors.sonic.net/apache/maven/maven-3/3.8.3/binaries/apache-maven-3.8.3-bin.tar.gz \ +&& tar xzvf apache-maven-3.8.3-bin.tar.gz +ENV PATH="/apache-maven-3.8.3/bin:$PATH" RUN export DEBIAN_FRONTEND=noninteractive \ && apt-get -qqy update \ diff --git a/checker/bin-devel/build.sh b/checker/bin-devel/build.sh index 0481a2b1c3..20d127b840 100755 --- a/checker/bin-devel/build.sh +++ b/checker/bin-devel/build.sh @@ -68,8 +68,8 @@ fi version=$("$_java" -version 2>&1 | head -1 | cut -d'"' -f2 | sed '/^1\./s///' | cut -d'.' -f1) if [[ "$version" -ge 9 ]]; then echo "Running: (cd ../jspecify/ && ./gradlew build)" - ## Try twice in case of network lossage. - (cd ../jspecify/ && ./gradlew build) || (sleep 60 && cd ../jspecify/ && ./gradlew build) + # If failure, retry in case the failure was due to network lossage. + (cd ../jspecify/ && export JDK_JAVA_OPTIONS='--add-opens jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED --add-opens jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED --add-opens jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED --add-opens jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED --add-opens jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED --add-opens jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED --add-opens jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED --add-opens jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED --add-opens jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED' && (./gradlew build || (sleep 60 && ./gradlew build))) echo "... done: (cd ../jspecify/ && ./gradlew build)" fi diff --git a/checker/bin-devel/git.pre-commit b/checker/bin-devel/git.pre-commit index f714056a7f..a799c5e2b6 100755 --- a/checker/bin-devel/git.pre-commit +++ b/checker/bin-devel/git.pre-commit @@ -11,7 +11,7 @@ set -e # Need to keep checked files in sync with getJavaFilesToFormat in build.gradle. # Otherwise `./gradlew reformat` might not reformat a file that this # hook complains about. -CHANGED_JAVA_FILES=$(git diff --staged --name-only --diff-filter=ACM | grep '\.java$' | grep -v '/jdk/' | grep -v 'stubparser/' | grep -v '/nullness-javac-errors/' | grep -v 'dataflow/manual/examples/' ) || true +CHANGED_JAVA_FILES=$(git diff --staged --name-only --diff-filter=ACM | grep '\.java$' | grep -v '/jdk/' | grep -v 'stubparser/' | grep -v '/nullness-javac-errors/' | grep -v 'dataflow/manual/examples/' | grep -v '/java17/') || true # echo "CHANGED_JAVA_FILES=${CHANGED_JAVA_FILES}" if [ -n "$CHANGED_JAVA_FILES" ]; then ./gradlew getCodeFormatScripts -q diff --git a/checker/bin-devel/test-cftests-all.sh b/checker/bin-devel/test-cftests-all.sh index df66c1a87f..745064e80a 100755 --- a/checker/bin-devel/test-cftests-all.sh +++ b/checker/bin-devel/test-cftests-all.sh @@ -10,8 +10,7 @@ export SHELLOPTS echo "SHELLOPTS=${SHELLOPTS}" SCRIPTDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" -# In newer shellcheck than 0.6.0, pass: "-P SCRIPTDIR" (literally) -# shellcheck disable=SC1090 +# shellcheck disable=SC1090 # In newer shellcheck than 0.6.0, pass: "-P SCRIPTDIR" (literally) source "$SCRIPTDIR"/build.sh diff --git a/checker/bin-devel/test-cftests-inference.sh b/checker/bin-devel/test-cftests-inference.sh index 0ead9a8aad..71900a0c71 100755 --- a/checker/bin-devel/test-cftests-inference.sh +++ b/checker/bin-devel/test-cftests-inference.sh @@ -7,8 +7,7 @@ export SHELLOPTS echo "SHELLOPTS=${SHELLOPTS}" SCRIPTDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" -# In newer shellcheck than 0.6.0, pass: "-P SCRIPTDIR" (literally) -# shellcheck disable=SC1090 +# shellcheck disable=SC1090 # In newer shellcheck than 0.6.0, pass: "-P SCRIPTDIR" (literally) source "$SCRIPTDIR"/build.sh diff --git a/checker/bin-devel/test-cftests-junit.sh b/checker/bin-devel/test-cftests-junit.sh index 3ac347f448..c79972fdef 100755 --- a/checker/bin-devel/test-cftests-junit.sh +++ b/checker/bin-devel/test-cftests-junit.sh @@ -7,8 +7,7 @@ export SHELLOPTS echo "SHELLOPTS=${SHELLOPTS}" SCRIPTDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" -# In newer shellcheck than 0.6.0, pass: "-P SCRIPTDIR" (literally) -# shellcheck disable=SC1090 +# shellcheck disable=SC1090# In newer shellcheck than 0.6.0, pass: "-P SCRIPTDIR" (literally) source "$SCRIPTDIR"/build.sh diff --git a/checker/bin-devel/test-cftests-nonjunit.sh b/checker/bin-devel/test-cftests-nonjunit.sh index fb203ad3c1..d9cbbc5b2f 100755 --- a/checker/bin-devel/test-cftests-nonjunit.sh +++ b/checker/bin-devel/test-cftests-nonjunit.sh @@ -7,8 +7,7 @@ export SHELLOPTS echo "SHELLOPTS=${SHELLOPTS}" SCRIPTDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" -# In newer shellcheck than 0.6.0, pass: "-P SCRIPTDIR" (literally) -# shellcheck disable=SC1090 +# shellcheck disable=SC1090# In newer shellcheck than 0.6.0, pass: "-P SCRIPTDIR" (literally) source "$SCRIPTDIR"/build.sh diff --git a/checker/bin-devel/test-daikon-part1.sh b/checker/bin-devel/test-daikon-part1.sh index de6cbc92e9..393971c168 100755 --- a/checker/bin-devel/test-daikon-part1.sh +++ b/checker/bin-devel/test-daikon-part1.sh @@ -7,8 +7,7 @@ export SHELLOPTS echo "SHELLOPTS=${SHELLOPTS}" SCRIPTDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" -# In newer shellcheck than 0.6.0, pass: "-P SCRIPTDIR" (literally) -# shellcheck disable=SC1090 +# shellcheck disable=SC1090# In newer shellcheck than 0.6.0, pass: "-P SCRIPTDIR" (literally) source "$SCRIPTDIR"/build.sh diff --git a/checker/bin-devel/test-daikon-part2.sh b/checker/bin-devel/test-daikon-part2.sh index 59afa2a2d1..6664d4fe54 100755 --- a/checker/bin-devel/test-daikon-part2.sh +++ b/checker/bin-devel/test-daikon-part2.sh @@ -7,8 +7,7 @@ export SHELLOPTS echo "SHELLOPTS=${SHELLOPTS}" SCRIPTDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" -# In newer shellcheck than 0.6.0, pass: "-P SCRIPTDIR" (literally) -# shellcheck disable=SC1090 +# shellcheck disable=SC1090 # In newer shellcheck than 0.6.0, pass: "-P SCRIPTDIR" (literally) source "$SCRIPTDIR"/build.sh diff --git a/checker/bin-devel/test-daikon.sh b/checker/bin-devel/test-daikon.sh index 7e660c7ee9..7179c6888a 100755 --- a/checker/bin-devel/test-daikon.sh +++ b/checker/bin-devel/test-daikon.sh @@ -7,8 +7,7 @@ export SHELLOPTS echo "SHELLOPTS=${SHELLOPTS}" SCRIPTDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" -# In newer shellcheck than 0.6.0, pass: "-P SCRIPTDIR" (literally) -# shellcheck disable=SC1090 +# shellcheck disable=SC1090 # In newer shellcheck than 0.6.0, pass: "-P SCRIPTDIR" (literally) source "$SCRIPTDIR"/build.sh diff --git a/checker/bin-devel/test-downstream.sh b/checker/bin-devel/test-downstream.sh index d1a9a51aa7..0b3d870239 100755 --- a/checker/bin-devel/test-downstream.sh +++ b/checker/bin-devel/test-downstream.sh @@ -7,8 +7,7 @@ export SHELLOPTS echo "SHELLOPTS=${SHELLOPTS}" SCRIPTDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" -# In newer shellcheck than 0.6.0, pass: "-P SCRIPTDIR" (literally) -# shellcheck disable=SC1090 +# shellcheck disable=SC1090 # In newer shellcheck than 0.6.0, pass: "-P SCRIPTDIR" (literally) source "$SCRIPTDIR"/build.sh diff --git a/checker/bin-devel/test-guava-formatter.sh b/checker/bin-devel/test-guava-formatter.sh index fd09305a67..4ae68302a1 100755 --- a/checker/bin-devel/test-guava-formatter.sh +++ b/checker/bin-devel/test-guava-formatter.sh @@ -7,8 +7,7 @@ export SHELLOPTS echo "SHELLOPTS=${SHELLOPTS}" SCRIPTDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" -# In newer shellcheck than 0.6.0, pass: "-P SCRIPTDIR" (literally) -# shellcheck disable=SC1090 +# shellcheck disable=SC1090 # In newer shellcheck than 0.6.0, pass: "-P SCRIPTDIR" (literally) source "$SCRIPTDIR"/build.sh diff --git a/checker/bin-devel/test-guava-index.sh b/checker/bin-devel/test-guava-index.sh index 0b8f8f904a..41f4ae94d6 100755 --- a/checker/bin-devel/test-guava-index.sh +++ b/checker/bin-devel/test-guava-index.sh @@ -7,8 +7,7 @@ export SHELLOPTS echo "SHELLOPTS=${SHELLOPTS}" SCRIPTDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" -# In newer shellcheck than 0.6.0, pass: "-P SCRIPTDIR" (literally) -# shellcheck disable=SC1090 +# shellcheck disable=SC1090 # In newer shellcheck than 0.6.0, pass: "-P SCRIPTDIR" (literally) source "$SCRIPTDIR"/build.sh diff --git a/checker/bin-devel/test-guava-interning.sh b/checker/bin-devel/test-guava-interning.sh index d1670b14ff..bbdda3b0f3 100755 --- a/checker/bin-devel/test-guava-interning.sh +++ b/checker/bin-devel/test-guava-interning.sh @@ -7,8 +7,7 @@ export SHELLOPTS echo "SHELLOPTS=${SHELLOPTS}" SCRIPTDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" -# In newer shellcheck than 0.6.0, pass: "-P SCRIPTDIR" (literally) -# shellcheck disable=SC1090 +# shellcheck disable=SC1090 # In newer shellcheck than 0.6.0, pass: "-P SCRIPTDIR" (literally) source "$SCRIPTDIR"/build.sh diff --git a/checker/bin-devel/test-guava-lock.sh b/checker/bin-devel/test-guava-lock.sh index 79528d9af0..a40e06ffab 100755 --- a/checker/bin-devel/test-guava-lock.sh +++ b/checker/bin-devel/test-guava-lock.sh @@ -7,8 +7,7 @@ export SHELLOPTS echo "SHELLOPTS=${SHELLOPTS}" SCRIPTDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" -# In newer shellcheck than 0.6.0, pass: "-P SCRIPTDIR" (literally) -# shellcheck disable=SC1090 +# shellcheck disable=SC1090 # In newer shellcheck than 0.6.0, pass: "-P SCRIPTDIR" (literally) source "$SCRIPTDIR"/build.sh diff --git a/checker/bin-devel/test-guava-nullness.sh b/checker/bin-devel/test-guava-nullness.sh index d14f17f075..922b67a76f 100755 --- a/checker/bin-devel/test-guava-nullness.sh +++ b/checker/bin-devel/test-guava-nullness.sh @@ -7,8 +7,7 @@ export SHELLOPTS echo "SHELLOPTS=${SHELLOPTS}" SCRIPTDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" -# In newer shellcheck than 0.6.0, pass: "-P SCRIPTDIR" (literally) -# shellcheck disable=SC1090 +# shellcheck disable=SC1090 # In newer shellcheck than 0.6.0, pass: "-P SCRIPTDIR" (literally) source "$SCRIPTDIR"/build.sh diff --git a/checker/bin-devel/test-guava-regex.sh b/checker/bin-devel/test-guava-regex.sh index 0b7f662c5b..ac5a66ee82 100755 --- a/checker/bin-devel/test-guava-regex.sh +++ b/checker/bin-devel/test-guava-regex.sh @@ -7,8 +7,7 @@ export SHELLOPTS echo "SHELLOPTS=${SHELLOPTS}" SCRIPTDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" -# In newer shellcheck than 0.6.0, pass: "-P SCRIPTDIR" (literally) -# shellcheck disable=SC1090 +# shellcheck disable=SC1090 # In newer shellcheck than 0.6.0, pass: "-P SCRIPTDIR" (literally) source "$SCRIPTDIR"/build.sh diff --git a/checker/bin-devel/test-guava-signature.sh b/checker/bin-devel/test-guava-signature.sh index 3f6e5f7109..f4494b9cb9 100755 --- a/checker/bin-devel/test-guava-signature.sh +++ b/checker/bin-devel/test-guava-signature.sh @@ -7,8 +7,7 @@ export SHELLOPTS echo "SHELLOPTS=${SHELLOPTS}" SCRIPTDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" -# In newer shellcheck than 0.6.0, pass: "-P SCRIPTDIR" (literally) -# shellcheck disable=SC1090 +# shellcheck disable=SC1090 # In newer shellcheck than 0.6.0, pass: "-P SCRIPTDIR" (literally) source "$SCRIPTDIR"/build.sh diff --git a/checker/bin-devel/test-guava.sh b/checker/bin-devel/test-guava.sh index 558ce13163..4ede06b6f5 100755 --- a/checker/bin-devel/test-guava.sh +++ b/checker/bin-devel/test-guava.sh @@ -7,8 +7,7 @@ export SHELLOPTS echo "SHELLOPTS=${SHELLOPTS}" SCRIPTDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" -# In newer shellcheck than 0.6.0, pass: "-P SCRIPTDIR" (literally) -# shellcheck disable=SC1090 +# shellcheck disable=SC1090 # In newer shellcheck than 0.6.0, pass: "-P SCRIPTDIR" (literally) source "$SCRIPTDIR"/build.sh diff --git a/checker/bin-devel/test-misc.sh b/checker/bin-devel/test-misc.sh index 85f711e465..01189262ed 100755 --- a/checker/bin-devel/test-misc.sh +++ b/checker/bin-devel/test-misc.sh @@ -7,8 +7,7 @@ export SHELLOPTS echo "SHELLOPTS=${SHELLOPTS}" SCRIPTDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" -# In newer shellcheck than 0.6.0, pass: "-P SCRIPTDIR" (literally) -# shellcheck disable=SC1090 +# shellcheck disable=SC1090 # In newer shellcheck than 0.6.0, pass: "-P SCRIPTDIR" (literally) source "$SCRIPTDIR"/build.sh PLUME_SCRIPTS="$SCRIPTDIR/.plume-scripts" diff --git a/checker/bin-devel/test-plume-lib.sh b/checker/bin-devel/test-plume-lib.sh index d1c2e7a196..f276c2578e 100755 --- a/checker/bin-devel/test-plume-lib.sh +++ b/checker/bin-devel/test-plume-lib.sh @@ -22,9 +22,10 @@ if [[ "${GROUPARG}" == "require-javadoc" ]]; then PACKAGES=("${GROUPARG}"); fi if [[ "${GROUPARG}" == "signature-util" ]]; then PACKAGES=("${GROUPARG}"); fi if [[ "${GROUPARG}" == "all" ]] || [[ "${GROUPARG}" == "" ]]; then if java -version 2>&1 | grep version | grep 1.8 ; then - PACKAGES=(bcel-util bibtex-clean html-pretty-print icalavailable lookup multi-version-control options plume-util require-javadoc) + # options does not compile under JDK 8 + PACKAGES=(bcel-util bibtex-clean html-pretty-print icalavailable lookup multi-version-control plume-util require-javadoc) else - PACKAGES=(bcel-util bibtex-clean html-pretty-print icalavailable lookup multi-version-control options plume-util) + PACKAGES=(bcel-util bibtex-clean html-pretty-print icalavailable lookup multi-version-control options plume-util require-javadoc) fi fi if [ -z ${PACKAGES+x} ]; then @@ -35,8 +36,7 @@ echo "PACKAGES=" "${PACKAGES[@]}" SCRIPTDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" -# In newer shellcheck than 0.6.0, pass: "-P SCRIPTDIR" (literally) -# shellcheck disable=SC1090 +# shellcheck disable=SC1090 # In newer shellcheck than 0.6.0, pass: "-P SCRIPTDIR" (literally) source "$SCRIPTDIR"/build.sh @@ -51,6 +51,6 @@ for PACKAGE in "${PACKAGES[@]}"; do echo "About to call ./gradlew --console=plain -PcfLocal compileJava" # Try twice in case of network lossage while downloading packages (e.g., from Maven Central). # A disadvantage is that if there is a real error in pluggable type-checking, this runs it twice - # and puts a delays in between. + # and puts a delay in between. (cd "${PACKAGEDIR}" && (./gradlew --console=plain -PcfLocal compileJava || (sleep 60 && ./gradlew --console=plain -PcfLocal compileJava))) done diff --git a/checker/bin-devel/test-typecheck.sh b/checker/bin-devel/test-typecheck.sh index b612d95d1f..b7aebfcb00 100755 --- a/checker/bin-devel/test-typecheck.sh +++ b/checker/bin-devel/test-typecheck.sh @@ -7,8 +7,7 @@ export SHELLOPTS echo "SHELLOPTS=${SHELLOPTS}" SCRIPTDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" -# In newer shellcheck than 0.6.0, pass: "-P SCRIPTDIR" (literally) -# shellcheck disable=SC1090 +# shellcheck disable=SC1090 # In newer shellcheck than 0.6.0, pass: "-P SCRIPTDIR" (literally) source "$SCRIPTDIR"/build.sh diff --git a/checker/bin-devel/wpi-plumelib/bcel-util.expected b/checker/bin-devel/wpi-plumelib/bcel-util.expected index 9c3d1132cd..d4c8b1ee9a 100644 --- a/checker/bin-devel/wpi-plumelib/bcel-util.expected +++ b/checker/bin-devel/wpi-plumelib/bcel-util.expected @@ -3,7 +3,7 @@ warning: No processor claimed any of these annotations: org.checkerframework.che # # This is in method fqBinaryNameToType, but there are no calls to it, so WPI didn't infer a type for its formal parameter. # -src/main/java/org/plumelib/bcelutil/BcelUtil.java:777: error: [argument] incompatible argument for parameter typename of parseFqBinaryName. +src/main/java/org/plumelib/bcelutil/BcelUtil.java:786: error: [argument] incompatible argument for parameter typename of parseFqBinaryName. Signatures.ClassnameAndDimensions.parseFqBinaryName(classname); ^ found : @SignatureUnknown String diff --git a/checker/bin-devel/wpi-plumelib/test-wpi-plumelib.sh b/checker/bin-devel/wpi-plumelib/test-wpi-plumelib.sh index 8a86e19963..b71aeb9838 100755 --- a/checker/bin-devel/wpi-plumelib/test-wpi-plumelib.sh +++ b/checker/bin-devel/wpi-plumelib/test-wpi-plumelib.sh @@ -45,6 +45,11 @@ clean_compile_output() { # Remove uninteresting output sed -i '/^warning: \[path\] bad path element /d' "$out" + sed -i '/^.*warning: Option --illegal-access is deprecated and will be removed in a future release./d' "$out" + sed -i '/^warning: \[options\] bootstrap class path not set/d' "$out" + + # Remove warning count because it can differ between JDK 8 and later JDKs due to the bootstrap warning: + sed -i '/^[0-9]* warning/d' "$out" # Remove directory names and line numbers sed -i 's/^[^ ]*\///' "$out" diff --git a/checker/bin/wpi-many.sh b/checker/bin/wpi-many.sh index 861ffafd72..e8a48be822 100755 --- a/checker/bin/wpi-many.sh +++ b/checker/bin/wpi-many.sh @@ -45,28 +45,39 @@ echo "Starting wpi-many.sh." # check required arguments and environment variables: -if [ "x${JAVA_HOME}" = "x" ]; then +# shellcheck disable=SC2153 # testing for JAVA_HOME, not a typo of JAVA8_HOME +if [ "${JAVA_HOME}" = "" ]; then has_java_home="no" else has_java_home="yes" fi -# testing for JAVA8_HOME, not an unintentional reference to JAVA_HOME -# shellcheck disable=SC2153 -if [ "x${JAVA8_HOME}" = "x" ]; then +# shellcheck disable=SC2153 # testing for JAVA8_HOME, not a typo of JAVA_HOME +if [ "${JAVA8_HOME}" = "" ]; then has_java8="no" else has_java8="yes" fi -# testing for JAVA11_HOME, not an unintentional reference to JAVA_HOME -# shellcheck disable=SC2153 -if [ "x${JAVA11_HOME}" = "x" ]; then +# shellcheck disable=SC2153 # testing for JAVA11_HOME, not a typo of JAVA_HOME +if [ "${JAVA11_HOME}" = "" ]; then has_java11="no" else has_java11="yes" fi +# shellcheck disable=SC2153 # testing for JAVA17_HOME, not a typo of JAVA_HOME +if [ "${JAVA17_HOME}" = "" ]; then + has_java17="no" +else + has_java17="yes" +fi + +if [ "${has_java_home}" = "yes" ] && [ ! -d "${JAVA_HOME}" ]; then + echo "JAVA_HOME is set to a non-existent directory ${JAVA_HOME}" + exit 1 +fi + if [ "${has_java_home}" = "yes" ]; then java_version=$("${JAVA_HOME}"/bin/java -version 2>&1 | head -1 | cut -d'"' -f2 | sed '/^1\./s///' | cut -d'.' -f1) if [ "${has_java8}" = "no" ] && [ "${java_version}" = 8 ]; then @@ -77,6 +88,10 @@ if [ "${has_java_home}" = "yes" ]; then export JAVA11_HOME="${JAVA_HOME}" has_java11="yes" fi + if [ "${has_java17}" = "no" ] && [ "${java_version}" = 17 ]; then + export JAVA17_HOME="${JAVA_HOME}" + has_java17="yes" + fi fi if [ "${has_java8}" = "yes" ] && [ ! -d "${JAVA8_HOME}" ]; then @@ -89,12 +104,17 @@ if [ "${has_java11}" = "yes" ] && [ ! -d "${JAVA11_HOME}" ]; then exit 1 fi -if [ "${has_java8}" = "no" ] && [ "${has_java11}" = "no" ]; then - echo "No Java 8 or 11 JDKs found. At least one of JAVA_HOME, JAVA8_HOME, or JAVA11_HOME must be set." +if [ "${has_java17}" = "yes" ] && [ ! -d "${JAVA17_HOME}" ]; then + echo "JAVA17_HOME is set to a non-existent directory ${JAVA17_HOME}" exit 1 fi -if [ "x${CHECKERFRAMEWORK}" = "x" ]; then +if [ "${has_java8}" = "no" ] && [ "${has_java11}" = "no" ] && [ "${has_java17}" = "no" ]; then + echo "No Java 8, 11, or 17 JDKs found. At least one of JAVA_HOME, JAVA8_HOME, JAVA11_HOME, or JAVA17_HOME must be set." + exit 1 +fi + +if [ "${CHECKERFRAMEWORK}" = "" ]; then echo "CHECKERFRAMEWORK is not set; it must be set to a locally-built Checker Framework. Please clone and build github.com/typetools/checker-framework" exit 2 fi @@ -104,27 +124,24 @@ if [ ! -d "${CHECKERFRAMEWORK}" ]; then exit 2 fi -if [ "x${OUTDIR}" = "x" ]; then +if [ "${OUTDIR}" = "" ]; then echo "You must specify an output directory using the -o argument." exit 3 fi -if [ "x${INLIST}" = "x" ]; then +if [ "${INLIST}" = "" ]; then echo "You must specify an input file using the -i argument." exit 4 fi -if [ "x${GRADLECACHEDIR}" = "x" ]; then +if [ "${GRADLECACHEDIR}" = "" ]; then GRADLECACHEDIR=".gradle" fi -if [ "x${SKIP_OR_DELETE_UNUSABLE}" = "x" ]; then +if [ "${SKIP_OR_DELETE_UNUSABLE}" = "" ]; then SKIP_OR_DELETE_UNUSABLE="delete" fi -JAVA_HOME_BACKUP="${JAVA_HOME}" -export JAVA_HOME="${JAVA11_HOME}" - ### Script echo "Finished configuring wpi-many.sh. Results will be placed in ${OUTDIR}-results/." @@ -138,6 +155,9 @@ cd "${OUTDIR}" || exit 5 while IFS='' read -r line || [ "$line" ] do + # Skip lines that start with "#". + [[ $line = \#* ]] && continue + REPOHASH=${line} REPO=$(echo "${REPOHASH}" | awk '{print $1}') @@ -264,9 +284,10 @@ else listpath=$(mktemp "/tmp/cloc-file-list-$(date +%Y%m%d-%H%M%S)-XXX.txt") # Compute lines of non-comment, non-blank Java code in the projects whose # results can be inspected by hand (that is, those that WPI succeeded on). - # Don't match arguments like "-J--add-opens=jdk.compiler/com.sun.tools.java". + # Don't match arguments like "-J--add-opens=jdk.compiler/com.sun.tools.java" + # or "--add-opens=jdk.compiler/com.sun.tools.java". # shellcheck disable=SC2046 - grep -oh "\S*\.java" $(cat "${OUTDIR}-results/results_available.txt") | sed "s/'//g" | grep -v '^\-J' | sort | uniq > "${listpath}" + grep -oh "\S*\.java" $(cat "${OUTDIR}-results/results_available.txt") | sed "s/'//g" | grep -v '^\-J' | grep -v '^\-\-add\-opens' | sort | uniq > "${listpath}" if [ ! -s "${listpath}" ] ; then echo "${listpath} has size zero" @@ -293,6 +314,4 @@ else fi fi -export JAVA_HOME="${JAVA_HOME_BACKUP}" - echo "Exiting wpi-many.sh. Results were placed in ${OUTDIR}-results/." diff --git a/checker/bin/wpi.sh b/checker/bin/wpi.sh index c2f6af76f4..f1203f51fc 100755 --- a/checker/bin/wpi.sh +++ b/checker/bin/wpi.sh @@ -39,28 +39,33 @@ echo "Starting wpi.sh." # check required arguments and environment variables: -if [ "x${JAVA_HOME}" = "x" ]; then +if [ "${JAVA_HOME}" = "" ]; then has_java_home="no" else has_java_home="yes" fi -# testing for JAVA8_HOME, not an unintentional reference to JAVA_HOME -# shellcheck disable=SC2153 -if [ "x${JAVA8_HOME}" = "x" ]; then +# shellcheck disable=SC2153 # testing for JAVA8_HOME, not a typo of JAVA_HOME +if [ "${JAVA8_HOME}" = "" ]; then has_java8="no" else has_java8="yes" fi -# testing for JAVA11_HOME, not an unintentional reference to JAVA_HOME -# shellcheck disable=SC2153 -if [ "x${JAVA11_HOME}" = "x" ]; then +# shellcheck disable=SC2153 # testing for JAVA11_HOME, not a typo of JAVA_HOME +if [ "${JAVA11_HOME}" = "" ]; then has_java11="no" else has_java11="yes" fi +# shellcheck disable=SC2153 # testing for JAVA17_HOME, not a typo of JAVA_HOME +if [ "${JAVA17_HOME}" = "" ]; then + has_java17="no" +else + has_java17="yes" +fi + if [ "${has_java_home}" = "yes" ]; then java_version=$("${JAVA_HOME}"/bin/java -version 2>&1 | head -1 | cut -d'"' -f2 | sed '/^1\./s///' | cut -d'.' -f1) if [ "${has_java8}" = "no" ] && [ "${java_version}" = 8 ]; then @@ -71,6 +76,10 @@ if [ "${has_java_home}" = "yes" ]; then export JAVA11_HOME="${JAVA_HOME}" has_java11="yes" fi + if [ "${has_java17}" = "no" ] && [ "${java_version}" = 17 ]; then + export JAVA17_HOME="${JAVA_HOME}" + has_java17="yes" + fi fi if [ "${has_java8}" = "yes" ] && [ ! -d "${JAVA8_HOME}" ]; then @@ -83,12 +92,17 @@ if [ "${has_java11}" = "yes" ] && [ ! -d "${JAVA11_HOME}" ]; then exit 7 fi -if [ "${has_java8}" = "no" ] && [ "${has_java11}" = "no" ]; then - echo "No Java 8 or 11 JDKs found. At least one of JAVA_HOME, JAVA8_HOME, or JAVA11_HOME must be set." +if [ "${has_java17}" = "yes" ] && [ ! -d "${JAVA17_HOME}" ]; then + echo "JAVA17_HOME is set to a non-existent directory ${JAVA17_HOME}" + exit 7 +fi + +if [ "${has_java8}" = "no" ] && [ "${has_java11}" = "no" ] && [ "${has_java17}" = "no" ]; then + echo "No Java 8, 11, or 17 JDKs found. At least one of JAVA_HOME, JAVA8_HOME, JAVA11_HOME, or JAVA17_HOME must be set." exit 8 fi -if [ "x${CHECKERFRAMEWORK}" = "x" ]; then +if [ "${CHECKERFRAMEWORK}" = "" ]; then echo "CHECKERFRAMEWORK is not set; it must be set to a locally-built Checker Framework. Please clone and build github.com/typetools/checker-framework" exit 2 fi @@ -98,7 +112,7 @@ if [ ! -d "${CHECKERFRAMEWORK}" ]; then exit 9 fi -if [ "x${DIR}" = "x" ]; then +if [ "${DIR}" = "" ]; then # echo "wpi.sh: no -d argument supplied, using the current directory." DIR=$(pwd) fi @@ -108,11 +122,11 @@ if [ ! -d "${DIR}" ]; then exit 4 fi -if [ "x${EXTRA_BUILD_ARGS}" = "x" ]; then +if [ "${EXTRA_BUILD_ARGS}" = "" ]; then EXTRA_BUILD_ARGS="" fi -if [ "x${GRADLECACHEDIR}" = "x" ]; then +if [ "${GRADLECACHEDIR}" = "" ]; then # Assume that each project should use its own gradle cache. This is more expensive, # but prevents crashes on distributed file systems, such as the UW CSE machines. GRADLECACHEDIR=".gradle" @@ -159,7 +173,12 @@ function configure_and_exec_dljc { if [ "${JAVA_HOME}" = "${JAVA8_HOME}" ]; then JDK_VERSION_ARG="--jdkVersion 8" + elif [ "${JAVA_HOME}" = "${JAVA11_HOME}" ]; then + JDK_VERSION_ARG="--jdkVersion 11" + elif [ "${JAVA_HOME}" = "${JAVA17_HOME}" ]; then + JDK_VERSION_ARG="--jdkVersion 17" else + # Default to the latest LTS release. (Probably better to compute the version.) JDK_VERSION_ARG="--jdkVersion 11" fi @@ -170,7 +189,7 @@ function configure_and_exec_dljc { # This command also includes "clean"; I'm not sure why it is necessary. DLJC_CMD="${DLJC} -t wpi ${JDK_VERSION_ARG} ${QUOTED_ARGS} -- ${BUILD_CMD}" - if [ ! "x${TIMEOUT}" = "x" ]; then + if [ ! "${TIMEOUT}" = "" ]; then TMP="${DLJC_CMD}" DLJC_CMD="timeout ${TIMEOUT} ${TMP}" fi @@ -178,8 +197,9 @@ function configure_and_exec_dljc { # Remove old DLJC output. rm -rf dljc-out - # ensure the project is clean before invoking DLJC - eval "${CLEAN_CMD}" < /dev/null > /dev/null 2>&1 + # Ensure the project is clean before invoking DLJC. + # If it fails, re-run without piping output to /dev/null. + eval "${CLEAN_CMD}" < /dev/null > /dev/null 2>&1 || eval "${CLEAN_CMD}" < /dev/null mkdir -p "${DIR}/dljc-out/" dljc_stdout=$(mktemp "${DIR}/dljc-out/dljc-stdout-$(date +%Y%m%d-%H%M%S)-XXX") @@ -187,8 +207,7 @@ function configure_and_exec_dljc { PATH_BACKUP="${PATH}" export PATH="${JAVA_HOME}/bin:${PATH}" - # use simpler syntax because this line was crashing mysteriously in CI, to get better debugging output - # shellcheck disable=SC2129 + # shellcheck disable=SC2129 # recommended syntax was crashing mysteriously in CI echo "WORKING DIR: $(pwd)" >> "$dljc_stdout" echo "JAVA_HOME: ${JAVA_HOME}" >> "$dljc_stdout" echo "PATH: ${PATH}" >> "$dljc_stdout" @@ -237,7 +256,7 @@ function configure_and_exec_dljc { #### Check and setup dependencies # Clone or update DLJC -if [ "${DLJC}x" = "x" ]; then +if [ "${DLJC}" = "" ]; then # The user did not set the DLJC environment variable. (cd "${SCRIPTDIR}"/../.. && ./gradlew getPlumeScripts -q) "${SCRIPTDIR}"/../bin-devel/.plume-scripts/git-clone-related kelloggm do-like-javac "${SCRIPTDIR}"/.do-like-javac @@ -263,22 +282,39 @@ rm -f -- "${DIR}/.cannot-run-wpi" cd "${DIR}" || exit 5 JAVA_HOME_BACKUP="${JAVA_HOME}" -if [ "${has_java11}" = "yes" ]; then - export JAVA_HOME="${JAVA11_HOME}" - configure_and_exec_dljc "$@" -elif [ "${has_java8}" = "yes" ]; then + +# For the first run, use the Java versions in ascending priority order: 8 if +# it's available, otherwise 11, otherwise 17. +if [ "${has_java8}" = "yes" ]; then export JAVA_HOME="${JAVA8_HOME}" - configure_and_exec_dljc "$@" +elif [ "${has_java11}" = "yes" ]; then + export JAVA_HOME="${JAVA11_HOME}" +elif [ "${has_java17}" = "yes" ]; then + export JAVA_HOME="${JAVA17_HOME}" +fi +configure_and_exec_dljc "$@" + +# If results aren't available after the first run, then re-run with Java 11 if +# it is available and the first run used Java 8 (since Java 8 has the highest priority, +# the first run using Java 8 is equivalent to Java 8 being available). +if [ "${WPI_RESULTS_AVAILABLE}" != "yes" ] && [ "${has_java11}" = "yes" ]; then + if [ "${has_java8}" = "yes" ]; then + export JAVA_HOME="${JAVA11_HOME}" + echo "couldn't build using Java 8; trying Java 11" + configure_and_exec_dljc "$@" + fi fi -if [ "${has_java11}" = "yes" ] && [ "${WPI_RESULTS_AVAILABLE}" != "yes" ]; then - # if running under Java 11 fails, try to run - # under Java 8 instead - if [ "${has_java8}" = "yes" ]; then - export JAVA_HOME="${JAVA8_HOME}" - echo "couldn't build using Java 11; trying Java 8" - configure_and_exec_dljc "$@" - fi +# If results still aren't available, then re-run with Java 17 if it is available +# and the first run used Java 8 or Java 11 (since Java 17 has the lowest priority, +# the first run using Java 8 or Java 11 is equivalent to either of these being +# available). +if [ "${WPI_RESULTS_AVAILABLE}" != "yes" ] && [ "${has_java17}" = "yes" ]; then + if [ "${has_java11}" = "yes" ] || [ "${has_java8}" = "yes" ]; then + export JAVA_HOME="${JAVA17_HOME}" + echo "couldn't build using Java 11 or Java 8; trying Java 17" + configure_and_exec_dljc "$@" + fi fi # support wpi-many.sh's ability to delete projects without usable results @@ -289,6 +325,11 @@ if [ "${WPI_RESULTS_AVAILABLE}" != "yes" ]; then echo "${WPI_RESULTS_AVAILABLE}" > "${DIR}/.cannot-run-wpi" fi -export JAVA_HOME="${JAVA_HOME_BACKUP}" +# reset JAVA_HOME to its initial value, which could be unset +if [ "${has_java_home}" = "yes" ]; then + export JAVA_HOME="${JAVA_HOME_BACKUP}" +else + unset JAVA_HOME +fi echo "Exiting wpi.sh." diff --git a/checker/build.gradle b/checker/build.gradle index 07dcd63970..ca0b3b4ab2 100644 --- a/checker/build.gradle +++ b/checker/build.gradle @@ -1,6 +1,6 @@ plugins { // https://github.com/n0mer/gradle-git-properties ; target is: generateGitProperties - id "com.gorylenko.gradle-git-properties" version "2.3.1" + id "com.gorylenko.gradle-git-properties" version "2.4.0-rc2" } sourceSets { main { @@ -19,6 +19,7 @@ sourcesJar { } configurations { + implementation.extendsFrom(annotatedGuava) fatJar { canBeConsumed = true canBeResolved = false @@ -44,18 +45,8 @@ dependencies { // As of 2019-12-16, the version of reflection-util in the Annotation // File Utilities takes priority over this version, in the fat jar // file. :-( So update it and re-build it locally when updating this. - implementation 'org.plumelib:reflection-util:1.0.3' - implementation 'org.plumelib:plume-util:1.5.5' - - // TODO: it's a bug that annotatedlib:guava requires the error_prone_annotations dependency. - // Update the next two version numbers in tandem. Get the Error Prone version from the "compile - // dependencies" section of https://mvnrepository.com/artifact/com.google.guava/guava/28.2-jre . - // (It isn't at https://mvnrepository.com/artifact/org.checkerframework.annotatedlib/guava/28.2-jre, which is the bug.) - implementation 'com.google.errorprone:error_prone_annotations:2.7.1' - implementation ('org.checkerframework.annotatedlib:guava:30.1.1-jre') { - // So long as Guava only uses annotations from checker-qual, excluding it should not cause problems. - exclude group: 'org.checkerframework' - } + implementation 'org.plumelib:reflection-util:1.0.4' + implementation 'org.plumelib:plume-util:1.5.8' // Dependencies added to "shadow" appear as dependencies in Maven Central. shadow project(':checker-qual') @@ -65,7 +56,7 @@ dependencies { testImplementation "com.google.auto.value:auto-value-annotations:1.7.4" testImplementation "com.google.auto.value:auto-value:1.7.4" testImplementation "com.ryanharter.auto.value:auto-value-parcel:0.2.9" - testImplementation "org.projectlombok:lombok:1.18.20" + testImplementation "org.projectlombok:lombok:1.18.22" // Called Methods Checker support for detecting misuses of AWS APIs testImplementation "com.amazonaws:aws-java-sdk-ec2" testImplementation "com.amazonaws:aws-java-sdk-kms" @@ -314,7 +305,15 @@ task jtregJdk11Tests(dependsOn: ':downloadJtreg', group: 'Verification') { "-samevm", "-javacoptions:-classpath ${tasks.shadowJar.archiveFile.get()}:${sourceSets.test.output.asPath}", "-vmoptions:-classpath ${tasks.shadowJar.archiveFile.get()}:${sourceSets.test.output.asPath}", + "-vmoptions:--add-opens=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED", + "-vmoptions:--add-opens=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED", "-vmoptions:--add-opens=jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED", + "-vmoptions:--add-opens=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED", + "-vmoptions:--add-opens=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED", + "-vmoptions:--add-opens=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED", + "-vmoptions:--add-opens=jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED", + "-vmoptions:--add-opens=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED", + "-vmoptions:--add-opens=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED", "-javacoptions:-classpath ${sourceSets.testannotations.output.asPath}", // Location of jtreg tests '.' @@ -497,7 +496,7 @@ task ainferTestCheckerAjavaTest(dependsOn: 'shadowJar', group: 'Verification') { } task ainferTestCheckerGenerateJaifs(type: Test) { - description 'Internal task. Users should run ainferNullnessJaifTest instead. This type-checks the ainfer-testchecker files with -Ainfer=jaifs to generate .jaif files' + description 'Internal task. Users should run ainferTestCheckerJaifTest instead. This type-checks the ainfer-testchecker files with -Ainfer=jaifs to generate .jaif files' dependsOn(compileTestJava) dependsOn(':checker-qual:jar') // For the Value Checker annotations. @@ -542,7 +541,8 @@ task ainferTestCheckerGenerateJaifs(type: Test) { executable "${afu}/scripts/insert-annotations-to-source" // Script argument -cp must precede Java program argument -i. // checker-qual is needed for Constant Value Checker annotations. - args = ['-cp', "${sourceSets.test.output.asPath}:${project(':checker-qual').tasks.jar.archivePath}"] + // Note that "/" works on Windows as well as on Linux. + args = ['-cp', "${sourceSets.test.output.asPath}:${project(':checker-qual').tasks.jar.archivePath}:" + file("tests/build/testclasses")] args += ['-i'] for (File jaif : jaifs) { args += [jaif.toString()] @@ -555,7 +555,7 @@ task ainferTestCheckerGenerateJaifs(type: Test) { } task ainferTestCheckerValidateJaifs(type: Test) { - description 'Internal task. Users should run ainferNullnessJaifTest instead. This type-checks the ainfer-testchecker files using the .jaif files generated by ainferTestCheckerGenerateJaifs' + description 'Internal task. Users should run ainferTestCheckerJaifTest instead. This type-checks the ainfer-testchecker files using the .jaif files generated by ainferTestCheckerGenerateJaifs' dependsOn(ainferTestCheckerGenerateJaifs) outputs.upToDateWhen { false } @@ -621,7 +621,8 @@ task ainferNullnessGenerateJaifs(type: Test) { exec { executable "${afu}/scripts/insert-annotations-to-source" // Script argument -cp must precede Java program argument -i. - args = ['-cp', "${sourceSets.test.output.asPath}"] + // Note that "/" works on Windows as well as on Linux. + args = ['-cp', "${sourceSets.test.output.asPath}:${project(':checker-qual').tasks.jar.archivePath}:" + file("tests/build/testclasses")] args += ['-i'] for (File jaif : jaifs) { args += [jaif.toString()] @@ -692,7 +693,7 @@ task wpiManyTest(group: "Verification") { '-i', "${project.projectDir}/tests/wpi-many/testin.txt", '-o', "${project.projectDir}/build/wpi-many-tests", '-s', - '--', '--checker', 'nullness,interning,lock,regex,signature,calledmethods' + '--', '--checker', 'nullness,interning,lock,regex,signature,calledmethods,resourceleak' } } catch (Exception e) { println("Failure: Running wpi-many.sh failed with a non-zero exit code.") @@ -710,7 +711,9 @@ task wpiManyTest(group: "Verification") { def typecheckFiles = fileTree(typecheckFilesDir).matching { include "**/*-typecheck.out" } - def expectedTypecheckFileCount = file("${project.projectDir}/tests/wpi-many/testin.txt").text.readLines().size() + def testinLines = file("${project.projectDir}/tests/wpi-many/testin.txt").text.readLines() + testinLines.removeIf { it.startsWith("#") } + def expectedTypecheckFileCount = testinLines.size() def actualTypecheckFileCount = typecheckFiles.size() if (actualTypecheckFileCount != expectedTypecheckFileCount) { println("Failure: Too few *-typecheck.out files in ${typecheckFilesDir}: " + @@ -746,7 +749,14 @@ task wpiManyTest(group: "Verification") { line.startsWith("Running ") // Warnings about bad path elements aren't related to WPI and are ignored. || line.startsWith("warning: [path]") - // Ignore the summary line that reports the total number of warnings. + // Ignore bootstrap classpath warning: + || line.startsWith("warning: [options] bootstrap") + // Ignore warnings about illegal access: + || line.contains("Option --illegal-access is deprecated") + // Ignore the warnings about --add-opens arguments to the JVM + || line.contains("warning: [options] --add-opens has no effect at compile time") + // Ignore the summary line that reports the total number of warnings (which can be single or plural). + || line.endsWith("warning") || line.endsWith("warnings")) { return; } diff --git a/checker/jtreg/nullness/Issue4948.java b/checker/jtreg/nullness/Issue4948.java new file mode 100644 index 0000000000..798918a908 --- /dev/null +++ b/checker/jtreg/nullness/Issue4948.java @@ -0,0 +1,1224 @@ +/* + * @test + * @summary Test case for issue #4948: https://github.com/typetools/checker-framework/issues/4948 + * + * @compile/timeout=30 -XDrawDiagnostics -Xlint:unchecked -processor org.checkerframework.checker.nullness.NullnessChecker -Alint Issue4948.java + */ +import java.util.stream.Stream; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.checker.nullness.qual.PolyNull; + +@SuppressWarnings("nullness") // Check for timeout. +public class Issue4948 { + + static class MyClass { + + // No type argumment inference. + static MyClass of2(@Nullable Object[]... p) { + throw new RuntimeException(); + } + + // Poly qualifiers + static MyClass<@PolyNull Object[]> of3(@PolyNull Object[]... p) { + throw new RuntimeException(); + } + } + + static Stream parseTest() { + return Stream.of( + new Object[] {"12:15:24", 125}, + new Object[] {"3:27", 5456}, + new Object[] {"-1:15:4:00", 54665453}, + new Object[] {"124:7:56:00", 45}, + new Object[] {"10", null}, + new Object[1], + new Object[] {"10:00:60:00", null}, + new Object[] {"10:00:00:05", null}, + new Object[] {"12#15:24", null}, + new Object[] {"12:15#24", null}, + new Object[] {"12:15:24#10", null}, + new Object[] {"x12:15:24", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"#####", -2}, + new Object[] {"*****", -1}, + new Object[] {"<<<<<", Integer.MIN_VALUE}, + new Object[] {">>>>>", Integer.MAX_VALUE}); + } + + static Stream parseTest2() { + return Stream.of( + new Object[] {"12:15:24", 125}, + new Object[] {"3:27", 5456}, + new Object[] {"-1:15:4:00", 54665453}, + new Object[] {"124:7:56:00", 45}, + new Object[] {"10", null}, + new Object[] {"10:74:00:00", null}, + new Object[] {"10:00:60:00", null}, + new Object[] {"10:00:00:05", null}, + new Object[] {"12#15:24", null}, + new Object[] {"12:15#24", null}, + new Object[] {"12:15:24#10", null}, + new Object[] {"x12:15:24", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"#####", -2}, + new Object[] {"*****", -1}, + new Object[] {"<<<<<", Integer.MIN_VALUE}, + new Object[] {">>>>>", Integer.MAX_VALUE}, + new Object[] {"12:15:24", 125}, + new Object[] {"3:27", 5456}, + new Object[] {"-1:15:4:00", 54665453}, + new Object[] {"124:7:56:00", 45}, + new Object[] {"10", null}, + new Object[] {"10:74:00:00", null}, + new Object[] {"10:00:60:00", null}, + new Object[] {"10:00:00:05", null}, + new Object[] {"12#15:24", null}, + new Object[] {"12:15#24", null}, + new Object[] {"12:15:24#10", null}, + new Object[] {"x12:15:24", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"#####", -2}, + new Object[] {"*****", -1}, + new Object[] {"<<<<<", Integer.MIN_VALUE}, + new Object[] {">>>>>", Integer.MAX_VALUE}); + } + + static Stream parseTest4() { + return Stream.of( + new Object[] {"12:15:24", 125}, + new Object[] {"3:27", 5456}, + new Object[] {"-1:15:4:00", 54665453}, + new Object[] {"124:7:56:00", 45}, + new Object[] {"10", null}, + new Object[] {"10:74:00:00", null}, + new Object[] {"10:00:60:00", null}, + new Object[] {"10:00:00:05", null}, + new Object[] {"12#15:24", null}, + new Object[] {"12:15#24", null}, + new Object[] {"12:15:24#10", null}, + new Object[] {"x12:15:24", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"#####", -2}, + new Object[] {"*****", -1}, + new Object[] {"<<<<<", Integer.MIN_VALUE}, + new Object[] {">>>>>", Integer.MAX_VALUE}, + new Object[] {"12:15:24", 125}, + new Object[] {"3:27", 5456}, + new Object[] {"-1:15:4:00", 54665453}, + new Object[] {"124:7:56:00", 45}, + new Object[] {"10", null}, + new Object[] {"10:74:00:00", null}, + new Object[] {"10:00:60:00", null}, + new Object[] {"10:00:00:05", null}, + new Object[] {"12#15:24", null}, + new Object[] {"12:15#24", null}, + new Object[] {"12:15:24#10", null}, + new Object[] {"x12:15:24", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"#####", -2}, + new Object[] {"*****", -1}, + new Object[] {"<<<<<", Integer.MIN_VALUE}, + new Object[] {">>>>>", Integer.MAX_VALUE}, + new Object[] {"12:15:24", 125}, + new Object[] {"3:27", 5456}, + new Object[] {"-1:15:4:00", 54665453}, + new Object[] {"124:7:56:00", 45}, + new Object[] {"10", null}, + new Object[] {"10:74:00:00", null}, + new Object[] {"10:00:60:00", null}, + new Object[] {"10:00:00:05", null}, + new Object[] {"12#15:24", null}, + new Object[] {"12:15#24", null}, + new Object[] {"12:15:24#10", null}, + new Object[] {"x12:15:24", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"#####", -2}, + new Object[] {"*****", -1}, + new Object[] {"<<<<<", Integer.MIN_VALUE}, + new Object[] {">>>>>", Integer.MAX_VALUE}, + new Object[] {"12:15:24", 125}, + new Object[] {"3:27", 5456}, + new Object[] {"-1:15:4:00", 54665453}, + new Object[] {"124:7:56:00", 45}, + new Object[] {"10", null}, + new Object[] {"10:74:00:00", null}, + new Object[] {"10:00:60:00", null}, + new Object[] {"10:00:00:05", null}, + new Object[] {"12#15:24", null}, + new Object[] {"12:15#24", null}, + new Object[] {"12:15:24#10", null}, + new Object[] {"x12:15:24", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"#####", -2}, + new Object[] {"*****", -1}, + new Object[] {"<<<<<", Integer.MIN_VALUE}, + new Object[] {">>>>>", Integer.MAX_VALUE}, + new Object[] {"12:15:24", 125}, + new Object[] {"3:27", 5456}, + new Object[] {"-1:15:4:00", 54665453}, + new Object[] {"124:7:56:00", 45}, + new Object[] {"10", null}, + new Object[] {"10:74:00:00", null}, + new Object[] {"10:00:60:00", null}, + new Object[] {"10:00:00:05", null}, + new Object[] {"12#15:24", null}, + new Object[] {"12:15#24", null}, + new Object[] {"12:15:24#10", null}, + new Object[] {"x12:15:24", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"#####", -2}, + new Object[] {"*****", -1}, + new Object[] {"<<<<<", Integer.MIN_VALUE}, + new Object[] {">>>>>", Integer.MAX_VALUE}, + new Object[] {"12:15:24", 125}, + new Object[] {"3:27", 5456}, + new Object[] {"-1:15:4:00", 54665453}, + new Object[] {"124:7:56:00", 45}, + new Object[] {"10", null}, + new Object[] {"10:74:00:00", null}, + new Object[] {"10:00:60:00", null}, + new Object[] {"10:00:00:05", null}, + new Object[] {"12#15:24", null}, + new Object[] {"12:15#24", null}, + new Object[] {"12:15:24#10", null}, + new Object[] {"x12:15:24", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"#####", -2}, + new Object[] {"*****", -1}, + new Object[] {"<<<<<", Integer.MIN_VALUE}, + new Object[] {">>>>>", Integer.MAX_VALUE}, + new Object[] {"12:15:24", 125}, + new Object[] {"3:27", 5456}, + new Object[] {"-1:15:4:00", 54665453}, + new Object[] {"124:7:56:00", 45}, + new Object[] {"10", null}, + new Object[] {"10:74:00:00", null}, + new Object[] {"10:00:60:00", null}, + new Object[] {"10:00:00:05", null}, + new Object[] {"12#15:24", null}, + new Object[] {"12:15#24", null}, + new Object[] {"12:15:24#10", null}, + new Object[] {"x12:15:24", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"#####", -2}, + new Object[] {"*****", -1}, + new Object[] {"<<<<<", Integer.MIN_VALUE}, + new Object[] {">>>>>", Integer.MAX_VALUE}); + } + + static MyClass parseTest11() { + return MyClass.of2( + new Object[] {"12:15:24", 125}, + new Object[] {"3:27", 5456}, + new Object[] {"-1:15:4:00", 54665453}, + new Object[] {"124:7:56:00", 45}, + new Object[] {"10", null}, + new Object[] {"10:74:00:00", null}, + new Object[] {"10:00:60:00", null}, + new Object[] {"10:00:00:05", null}, + new Object[] {"12#15:24", null}, + new Object[] {"12:15#24", null}, + new Object[] {"12:15:24#10", null}, + new Object[] {"x12:15:24", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"#####", -2}, + new Object[] {"*****", -1}, + new Object[] {"<<<<<", Integer.MIN_VALUE}, + new Object[] {">>>>>", Integer.MAX_VALUE}, + new Object[] {"12:15:24", 125}, + new Object[] {"3:27", 5456}, + new Object[] {"-1:15:4:00", 54665453}, + new Object[] {"124:7:56:00", 45}, + new Object[] {"10", null}, + new Object[] {"10:74:00:00", null}, + new Object[] {"10:00:60:00", null}, + new Object[] {"10:00:00:05", null}, + new Object[] {"12#15:24", null}, + new Object[] {"12:15#24", null}, + new Object[] {"12:15:24#10", null}, + new Object[] {"x12:15:24", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"#####", -2}, + new Object[] {"*****", -1}, + new Object[] {"<<<<<", Integer.MIN_VALUE}, + new Object[] {">>>>>", Integer.MAX_VALUE}, + new Object[] {"12:15:24", 125}, + new Object[] {"3:27", 5456}, + new Object[] {"-1:15:4:00", 54665453}, + new Object[] {"124:7:56:00", 45}, + new Object[] {"10", null}, + new Object[] {"10:74:00:00", null}, + new Object[] {"10:00:60:00", null}, + new Object[] {"10:00:00:05", null}, + new Object[] {"12#15:24", null}, + new Object[] {"12:15#24", null}, + new Object[] {"12:15:24#10", null}, + new Object[] {"x12:15:24", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"#####", -2}, + new Object[] {"*****", -1}, + new Object[] {"<<<<<", Integer.MIN_VALUE}, + new Object[] {">>>>>", Integer.MAX_VALUE}, + new Object[] {"12:15:24", 125}, + new Object[] {"3:27", 5456}, + new Object[] {"-1:15:4:00", 54665453}, + new Object[] {"124:7:56:00", 45}, + new Object[] {"10", null}, + new Object[] {"10:74:00:00", null}, + new Object[] {"10:00:60:00", null}, + new Object[] {"10:00:00:05", null}, + new Object[] {"12#15:24", null}, + new Object[] {"12:15#24", null}, + new Object[] {"12:15:24#10", null}, + new Object[] {"x12:15:24", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"#####", -2}, + new Object[] {"*****", -1}, + new Object[] {"<<<<<", Integer.MIN_VALUE}, + new Object[] {">>>>>", Integer.MAX_VALUE}); + } + + static MyClass parseTestB4() { + return MyClass.of3( + new Object[] {"12:15:24", 125}, + new Object[] {"3:27", 5456}, + new Object[] {"-1:15:4:00", 54665453}, + new Object[] {"124:7:56:00", 45}, + new Object[] {"10", null}, + new Object[] {"10:74:00:00", null}, + new Object[] {"10:00:60:00", null}, + new Object[] {"10:00:00:05", null}, + new Object[] {"12#15:24", null}, + new Object[] {"12:15#24", null}, + new Object[] {"12:15:24#10", null}, + new Object[] {"x12:15:24", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"#####", -2}, + new Object[] {"*****", -1}, + new Object[] {"<<<<<", Integer.MIN_VALUE}, + new Object[] {">>>>>", Integer.MAX_VALUE}, + new Object[] {"12:15:24", 125}, + new Object[] {"3:27", 5456}, + new Object[] {"-1:15:4:00", 54665453}, + new Object[] {"124:7:56:00", 45}, + new Object[] {"10", null}, + new Object[] {"10:74:00:00", null}, + new Object[] {"10:00:60:00", null}, + new Object[] {"10:00:00:05", null}, + new Object[] {"12#15:24", null}, + new Object[] {"12:15#24", null}, + new Object[] {"12:15:24#10", null}, + new Object[] {"x12:15:24", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"#####", -2}, + new Object[] {"*****", -1}, + new Object[] {"<<<<<", Integer.MIN_VALUE}, + new Object[] {">>>>>", Integer.MAX_VALUE}, + new Object[] {"12:15:24", 125}, + new Object[] {"3:27", 5456}, + new Object[] {"-1:15:4:00", 54665453}, + new Object[] {"124:7:56:00", 45}, + new Object[] {"10", null}, + new Object[] {"10:74:00:00", null}, + new Object[] {"10:00:60:00", null}, + new Object[] {"10:00:00:05", null}, + new Object[] {"12#15:24", null}, + new Object[] {"12:15#24", null}, + new Object[] {"12:15:24#10", null}, + new Object[] {"x12:15:24", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"#####", -2}, + new Object[] {"*****", -1}, + new Object[] {"<<<<<", Integer.MIN_VALUE}, + new Object[] {">>>>>", Integer.MAX_VALUE}, + new Object[] {"12:15:24", 125}, + new Object[] {"3:27", 5456}, + new Object[] {"-1:15:4:00", 54665453}, + new Object[] {"124:7:56:00", 45}, + new Object[] {"10", null}, + new Object[] {"10:74:00:00", null}, + new Object[] {"10:00:60:00", null}, + new Object[] {"10:00:00:05", null}, + new Object[] {"12#15:24", null}, + new Object[] {"12:15#24", null}, + new Object[] {"12:15:24#10", null}, + new Object[] {"x12:15:24", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"#####", -2}, + new Object[] {"*****", -1}, + new Object[] {"<<<<<", Integer.MIN_VALUE}, + new Object[] {">>>>>", Integer.MAX_VALUE}, + new Object[] {"12:15:24", 125}, + new Object[] {"3:27", 5456}, + new Object[] {"-1:15:4:00", 54665453}, + new Object[] {"124:7:56:00", 45}, + new Object[] {"10", null}, + new Object[] {"10:74:00:00", null}, + new Object[] {"10:00:60:00", null}, + new Object[] {"10:00:00:05", null}, + new Object[] {"12#15:24", null}, + new Object[] {"12:15#24", null}, + new Object[] {"12:15:24#10", null}, + new Object[] {"x12:15:24", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"#####", -2}, + new Object[] {"*****", -1}, + new Object[] {"<<<<<", Integer.MIN_VALUE}, + new Object[] {">>>>>", Integer.MAX_VALUE}); + } + + static Stream parseTestB9() { + return Stream.of( + new Object[] {"12:15:24", 125}, + new Object[] {"3:27", 5456}, + new Object[] {"-1:15:4:00", 54665453}, + new Object[] {"124:7:56:00", 45}, + new Object[] {"10", null}, + new Object[] {"10:74:00:00", null}, + new Object[] {"10:00:60:00", null}, + new Object[] {"10:00:00:05", null}, + new Object[] {"12#15:24", null}, + new Object[] {"12:15#24", null}, + new Object[] {"12:15:24#10", null}, + new Object[] {"x12:15:24", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"#####", -2}, + new Object[] {"*****", -1}, + new Object[] {"<<<<<", Integer.MIN_VALUE}, + new Object[] {">>>>>", Integer.MAX_VALUE}); + } + + static Stream parseTestB10() { + return Stream.of( + new Object[] {"12:15:24", 125}, + new Object[] {"3:27", 5456}, + new Object[] {"-1:15:4:00", 54665453}, + new Object[] {"124:7:56:00", 45}, + new Object[] {"10", null}, + new Object[] {"10:74:00:00", null}, + new Object[] {"10:00:60:00", null}, + new Object[] {"10:00:00:05", null}, + new Object[] {"12#15:24", null}, + new Object[] {"12:15#24", null}, + new Object[] {"12:15:24#10", null}, + new Object[] {"x12:15:24", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"#####", -2}, + new Object[] {"*****", -1}, + new Object[] {"<<<<<", Integer.MIN_VALUE}, + new Object[] {">>>>>", Integer.MAX_VALUE}); + } + + static Stream parseTestB11() { + return Stream.of( + new Object[] {"12:15:24", 125}, + new Object[] {"3:27", 5456}, + new Object[] {"-1:15:4:00", 54665453}, + new Object[] {"124:7:56:00", 45}, + new Object[] {"10", null}, + new Object[] {"10:74:00:00", null}, + new Object[] {"10:00:60:00", null}, + new Object[] {"10:00:00:05", null}, + new Object[] {"12#15:24", null}, + new Object[] {"12:15#24", null}, + new Object[] {"12:15:24#10", null}, + new Object[] {"x12:15:24", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"#####", -2}, + new Object[] {"*****", -1}, + new Object[] {"<<<<<", Integer.MIN_VALUE}, + new Object[] {">>>>>", Integer.MAX_VALUE}); + } +} diff --git a/checker/jtreg/nullness/PersistUtil.java b/checker/jtreg/nullness/PersistUtil.java index 51f92dec39..8a9f313c11 100644 --- a/checker/jtreg/nullness/PersistUtil.java +++ b/checker/jtreg/nullness/PersistUtil.java @@ -2,10 +2,6 @@ // is added to the invocation of the compiler! // TODO: add a @Processor method-annotation to parameterize -/** - * This class has auxiliary methods to compile a class and return its classfile. It is used by - * defaultPersists/Driver and inheritDeclAnnoPersist/Driver. - */ import com.sun.tools.classfile.ClassFile; import java.io.BufferedWriter; import java.io.File; @@ -17,8 +13,14 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.lang.reflect.Method; +import java.nio.file.Files; +import java.nio.file.StandardCopyOption; import java.util.StringJoiner; +/** + * This class has auxiliary methods to compile a class and return its classfile. It is used by + * defaultPersists/Driver and inheritDeclAnnoPersist/Driver. + */ public class PersistUtil { public static String testClassOf(Method m) { @@ -65,7 +67,25 @@ public static File compileTestFile(File f, String testClass) { path = ""; } - return new File(path + testClass + ".class"); + File result = new File(path + testClass + ".class"); + + // This diagnostic code preserves temporary files and prints the paths where they are preserved. + if (false) { + try { + File tempDir = new File(System.getProperty("java.io.tmpdir")); + File fCopy = File.createTempFile("FCopy", ".java", tempDir); + File resultCopy = File.createTempFile("FCopy", ".class", tempDir); + // REPLACE_EXISTING is essential in the `Files.copy()` calls because createTempFile actually + // creates a file in addition to returning its name. + Files.copy(f.toPath(), fCopy.toPath(), StandardCopyOption.REPLACE_EXISTING); + Files.copy(result.toPath(), resultCopy.toPath(), StandardCopyOption.REPLACE_EXISTING); + System.out.printf("comileTestFile: copied to %s %s%n", fCopy, resultCopy); + } catch (IOException e) { + throw new Error(e); + } + } + + return result; } public static String wrap(String compact) { diff --git a/checker/jtreg/nullness/defaultsPersist/Classes.java b/checker/jtreg/nullness/defaultsPersist/Classes.java index 582ce0648f..49c0903ca6 100644 --- a/checker/jtreg/nullness/defaultsPersist/Classes.java +++ b/checker/jtreg/nullness/defaultsPersist/Classes.java @@ -71,6 +71,16 @@ public String extendsDefault3() { type = CLASS_TYPE_PARAMETER_BOUND, paramIndex = 0, boundIndex = 0), + // Annotations on the implicit constructor, which the test ignores. + // @TADescription( + // annotation = "org/checkerframework/checker/nullness/qual/NonNull", + // type = METHOD_RETURN), + // @TADescription( + // annotation = "org/checkerframework/checker/initialization/qual/UnderInitialization", + // type = METHOD_RETURN), + // @TADescription( + // annotation = "org/checkerframework/checker/nullness/qual/UnknownKeyFor", + // type = METHOD_RETURN), }) public String typeParams1() { return "class Test {}"; @@ -104,6 +114,16 @@ public String typeParams1() { type = CLASS_TYPE_PARAMETER_BOUND, paramIndex = 0, boundIndex = 0), + // Annotations on the implicit constructor, which the test ignores. + // @TADescription( + // annotation = "org/checkerframework/checker/nullness/qual/NonNull", + // type = METHOD_RETURN), + // @TADescription( + // annotation = "org/checkerframework/checker/initialization/qual/UnderInitialization", + // type = METHOD_RETURN), + // @TADescription( + // annotation = "org/checkerframework/checker/nullness/qual/UnknownKeyFor", + // type = METHOD_RETURN), }) public String typeParams2() { return "class Test {}"; @@ -137,6 +157,16 @@ public String typeParams2() { type = CLASS_TYPE_PARAMETER_BOUND, paramIndex = 0, boundIndex = 1), + // Annotations on the implicit constructor, which the test ignores. + // @TADescription( + // annotation = "org/checkerframework/checker/nullness/qual/NonNull", + // type = METHOD_RETURN), + // @TADescription( + // annotation = "org/checkerframework/checker/initialization/qual/UnderInitialization", + // type = METHOD_RETURN), + // @TADescription( + // annotation = "org/checkerframework/checker/nullness/qual/UnknownKeyFor", + // type = METHOD_RETURN), }) public String typeParams3() { return "class Test> {}"; @@ -197,6 +227,16 @@ public String typeParams3() { type = CLASS_TYPE_PARAMETER_BOUND, paramIndex = 1, boundIndex = 1), + // Annotations on the implicit constructor, which the test ignores. + // @TADescription( + // annotation = "org/checkerframework/checker/nullness/qual/NonNull", + // type = METHOD_RETURN), + // @TADescription( + // annotation = "org/checkerframework/checker/initialization/qual/UnderInitialization", + // type = METHOD_RETURN), + // @TADescription( + // annotation = "org/checkerframework/checker/nullness/qual/UnknownKeyFor", + // type = METHOD_RETURN), }) public String typeParams4() { return "class Test> {}"; diff --git a/checker/jtreg/nullness/defaultsPersist/Driver.java b/checker/jtreg/nullness/defaultsPersist/Driver.java index e97b20d1c3..45f2a78bec 100644 --- a/checker/jtreg/nullness/defaultsPersist/Driver.java +++ b/checker/jtreg/nullness/defaultsPersist/Driver.java @@ -22,8 +22,9 @@ public class Driver { private static final PrintStream out = System.out; + // The argument is in the format expected by Class.forName(). public static void main(String[] args) throws Exception { - if (args.length == 0 || args.length > 1) { + if (args.length != 1) { throw new IllegalArgumentException("Usage: java Driver "); } String name = args[0]; @@ -51,8 +52,17 @@ protected void runDriver(Object object) throws Exception { String compact = (String) method.invoke(object); String fullFile = PersistUtil.wrap(compact); ClassFile cf = PersistUtil.compileAndReturn(fullFile, testClass); - List actual = ReferenceInfoUtil.extendedAnnotationsOf(cf); - ReferenceInfoUtil.compare(expected, actual, cf); + boolean ignoreConstructors = !clazz.getName().equals("Constructors"); + List actual = + ReferenceInfoUtil.extendedAnnotationsOf(cf, ignoreConstructors); + String diagnostic = + String.join( + "; ", + "Tests for " + clazz.getName(), + "compact=" + compact, + "fullFile=" + fullFile, + "testClass=" + testClass); + ReferenceInfoUtil.compare(expected, actual, cf, diagnostic); out.println("PASSED: " + method.getName()); ++passed; } catch (Throwable e) { diff --git a/checker/jtreg/nullness/defaultsPersist/ReferenceInfoUtil.java b/checker/jtreg/nullness/defaultsPersist/ReferenceInfoUtil.java index 97deb42c3b..bde0ddc1ce 100644 --- a/checker/jtreg/nullness/defaultsPersist/ReferenceInfoUtil.java +++ b/checker/jtreg/nullness/defaultsPersist/ReferenceInfoUtil.java @@ -1,6 +1,6 @@ // Keep somewhat in sync with // langtools/test/tools/javac/annotations/typeAnnotations/referenceinfos/ReferenceInfoUtil.java -// Adapted to handled the same type qualifier appearing multiple times. +// Adapted to handle the same type qualifier appearing multiple times. import com.sun.tools.classfile.Attribute; import com.sun.tools.classfile.ClassFile; @@ -20,14 +20,28 @@ public class ReferenceInfoUtil { public static final int IGNORE_VALUE = -321; - public static List extendedAnnotationsOf(ClassFile cf) { + /** If true, don't collect annotations on constructors. */ + boolean ignoreConstructors; + + /** + * Creates a new ReferenceInfoUtil. + * + * @param ignoreConstructors if true, don't collect annotations on constructor + */ + public ReferenceInfoUtil(boolean ignoreConstructors) { + this.ignoreConstructors = ignoreConstructors; + } + + public static List extendedAnnotationsOf( + ClassFile cf, boolean ignoreConstructors) { + ReferenceInfoUtil riu = new ReferenceInfoUtil(ignoreConstructors); List annos = new ArrayList<>(); - findAnnotations(cf, annos); + riu.findAnnotations(cf, annos); return annos; } /////////////////// Extract type annotations ////////////////// - private static void findAnnotations(ClassFile cf, List annos) { + private void findAnnotations(ClassFile cf, List annos) { findAnnotations(cf, Attribute.RuntimeVisibleTypeAnnotations, annos); findAnnotations(cf, Attribute.RuntimeInvisibleTypeAnnotations, annos); @@ -35,6 +49,19 @@ private static void findAnnotations(ClassFile cf, List annos) { findAnnotations(cf, f, annos); } for (Method m : cf.methods) { + String methodName; + try { + methodName = m.getName(cf.constant_pool); + } catch (Exception e) { + throw new Error(e); + } + // This method, `findAnnotations`, aims to extract annotations from one method. + // In JDK 17, constructors are included in ClassFile.methods(); in JDK 11, they are not. + // Therefore, this if statement is required in JDK 17, and has no effect in JDK 11. + if (ignoreConstructors && methodName.equals("")) { + continue; + } + findAnnotations(cf, m, annos); } } @@ -189,10 +216,12 @@ private static TypeAnnotation findAnnotation( public static boolean compare( List> expectedAnnos, List actualAnnos, - ClassFile cf) + ClassFile cf, + String diagnostic) throws InvalidIndex, UnexpectedEntry { if (actualAnnos.size() != expectedAnnos.size()) { - throw new ComparisonException("Wrong number of annotations", expectedAnnos, actualAnnos); + throw new ComparisonException( + "Wrong number of annotations in " + cf + "; " + diagnostic, expectedAnnos, actualAnnos); } for (Pair e : expectedAnnos) { @@ -201,7 +230,12 @@ public static boolean compare( TypeAnnotation actual = findAnnotation(aName, expected, actualAnnos, cf); if (actual == null) { throw new ComparisonException( - "Expected annotation not found: " + aName + " position: " + expected, + "Expected annotation not found: " + + aName + + " position: " + + expected + + "; " + + diagnostic, expectedAnnos, actualAnnos); } @@ -226,15 +260,8 @@ public ComparisonException( } public String toString() { - return String.join( - System.lineSeparator(), - super.toString(), - "\tExpected: " - + expected.size() - + " annotations; but found: " - + found.size() - + " annotations", - " Expected: " + expected, - " Found: " + found); + return String.format( + "%s%n Expected (%d): %s%s Found (%d): %s", + super.toString(), expected.size(), expected, found.size(), found); } } diff --git a/checker/jtreg/nullness/inheritDeclAnnoPersist/Driver.java b/checker/jtreg/nullness/inheritDeclAnnoPersist/Driver.java index 1651a1095b..28e60064ba 100644 --- a/checker/jtreg/nullness/inheritDeclAnnoPersist/Driver.java +++ b/checker/jtreg/nullness/inheritDeclAnnoPersist/Driver.java @@ -16,8 +16,9 @@ public class Driver { private static final PrintStream out = System.out; + // The argument is in the format expected by Class.forName(). public static void main(String[] args) throws Exception { - if (args.length == 0 || args.length > 1) { + if (args.length != 1) { throw new IllegalArgumentException("Usage: java Driver "); } String name = args[0]; @@ -46,7 +47,14 @@ protected void runDriver(Object object) throws Exception { String fullFile = PersistUtil.wrap(compact); ClassFile cf = PersistUtil.compileAndReturn(fullFile, testClass); List actual = ReferenceInfoUtil.extendedAnnotationsOf(cf); - ReferenceInfoUtil.compare(expected, actual, cf); + String diagnostic = + String.join( + "; ", + "Tests for " + clazz.getName(), + "compact=" + compact, + "fullFile=" + fullFile, + "testClass=" + testClass); + ReferenceInfoUtil.compare(expected, actual, cf, diagnostic); out.println("PASSED: " + method.getName()); ++passed; } catch (Throwable e) { diff --git a/checker/jtreg/nullness/inheritDeclAnnoPersist/ReferenceInfoUtil.java b/checker/jtreg/nullness/inheritDeclAnnoPersist/ReferenceInfoUtil.java index 032fc576ba..697d995bf1 100644 --- a/checker/jtreg/nullness/inheritDeclAnnoPersist/ReferenceInfoUtil.java +++ b/checker/jtreg/nullness/inheritDeclAnnoPersist/ReferenceInfoUtil.java @@ -1,6 +1,6 @@ // Keep somewhat in sync with // langtools/test/tools/javac/annotations/typeAnnotations/referenceinfos/ReferenceInfoUtil.java -// Adapted to handled the same type qualifier appearing multiple times. +// Adapted to handle the same type qualifier appearing multiple times. import com.sun.tools.classfile.Annotation; import com.sun.tools.classfile.Attribute; @@ -60,16 +60,20 @@ private static Annotation findAnnotation(String name, List annotatio } public static boolean compare( - List expectedAnnos, List actualAnnos, ClassFile cf) + List expectedAnnos, List actualAnnos, ClassFile cf, String diagnostic) throws InvalidIndex, UnexpectedEntry { if (actualAnnos.size() != expectedAnnos.size()) { - throw new ComparisonException("Wrong number of annotations", expectedAnnos, actualAnnos, cf); + throw new ComparisonException( + "Wrong number of annotations; " + diagnostic, expectedAnnos, actualAnnos, cf); } for (String annoName : expectedAnnos) { Annotation anno = findAnnotation(annoName, actualAnnos, cf); if (anno == null) { throw new ComparisonException( - "Expected annotation not found: " + annoName, expectedAnnos, actualAnnos, cf); + "Expected annotation not found: " + annoName + "; " + diagnostic, + expectedAnnos, + actualAnnos, + cf); } } return true; diff --git a/checker/jtreg/nullness/issue2173/View.out b/checker/jtreg/nullness/issue2173/View.out index 7edd091e48..2fd6dd235b 100644 --- a/checker/jtreg/nullness/issue2173/View.out +++ b/checker/jtreg/nullness/issue2173/View.out @@ -1,5 +1,3 @@ - compiler.warn.proc.messager: (invalid.annotation.location.bytecode) - compiler.warn.proc.messager: (invalid.annotation.location.bytecode) -- compiler.warn.proc.messager: (invalid.annotation.location.bytecode) -- compiler.warn.proc.messager: (invalid.annotation.location.bytecode) -4 warnings +2 warnings diff --git a/checker/src/main/java/org/checkerframework/checker/calledmethods/CalledMethodsAnalysis.java b/checker/src/main/java/org/checkerframework/checker/calledmethods/CalledMethodsAnalysis.java new file mode 100644 index 0000000000..7fbe25092a --- /dev/null +++ b/checker/src/main/java/org/checkerframework/checker/calledmethods/CalledMethodsAnalysis.java @@ -0,0 +1,52 @@ +package org.checkerframework.checker.calledmethods; + +import com.sun.tools.javac.code.Type; +import java.util.HashSet; +import java.util.Set; +import javax.lang.model.type.TypeMirror; +import org.checkerframework.common.basetype.BaseTypeChecker; +import org.checkerframework.framework.flow.CFAnalysis; + +/** + * The analysis for the Called Methods Checker. The analysis is specialized to ignore certain + * exception types; see {@link #isIgnoredExceptionType(TypeMirror)}. + */ +public class CalledMethodsAnalysis extends CFAnalysis { + + /** + * Creates a new {@code CalledMethodsAnalysis}. + * + * @param checker the checker + * @param factory the factory + */ + protected CalledMethodsAnalysis( + BaseTypeChecker checker, CalledMethodsAnnotatedTypeFactory factory) { + super(checker, factory); + // Use the Nullness Checker. + ignoredExceptionTypes.add("java.lang.NullPointerException"); + // Ignore run-time errors, which cannot be predicted statically. Doing so is unsound + // in the sense that they could still occur - e.g., the program could run out of memory - + // but if they did, the checker's results would be useless anyway. + ignoredExceptionTypes.add("java.lang.Error"); + ignoredExceptionTypes.add("java.lang.RuntimeException"); + } + + /** + * The fully-qualified names of the exception types that are ignored by this checker when + * computing dataflow stores. + */ + protected final Set ignoredExceptionTypes = new HashSet<>(); + + /** + * Ignore exceptional control flow due to ignored exception types. + * + * @param exceptionType exception type + * @return {@code true} if {@code exceptionType} is a member of {@link #ignoredExceptionTypes}, + * {@code false} otherwise + */ + @Override + protected boolean isIgnoredExceptionType(TypeMirror exceptionType) { + return ignoredExceptionTypes.contains( + ((Type) exceptionType).tsym.getQualifiedName().toString()); + } +} diff --git a/checker/src/main/java/org/checkerframework/checker/calledmethods/CalledMethodsAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/calledmethods/CalledMethodsAnnotatedTypeFactory.java index ea0aa8874b..b1e3ff71bc 100644 --- a/checker/src/main/java/org/checkerframework/checker/calledmethods/CalledMethodsAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/calledmethods/CalledMethodsAnnotatedTypeFactory.java @@ -6,13 +6,11 @@ import com.sun.source.tree.Tree; import java.util.ArrayList; import java.util.Collection; -import java.util.Collections; import java.util.List; import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.Element; import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.TypeElement; -import javax.lang.model.element.VariableElement; import javax.lang.model.type.TypeKind; import javax.lang.model.type.TypeMirror; import org.checkerframework.checker.calledmethods.builder.AutoValueSupport; @@ -29,7 +27,8 @@ import org.checkerframework.common.value.ValueAnnotatedTypeFactory; import org.checkerframework.common.value.ValueChecker; import org.checkerframework.common.value.ValueCheckerUtils; -import org.checkerframework.dataflow.expression.JavaExpression; +import org.checkerframework.dataflow.analysis.Analysis; +import org.checkerframework.dataflow.analysis.Analysis.BeforeOrAfter; import org.checkerframework.framework.type.AnnotatedTypeFactory; import org.checkerframework.framework.type.AnnotatedTypeMirror; import org.checkerframework.framework.type.treeannotator.ListTreeAnnotator; @@ -355,6 +354,11 @@ public Void visitExecutable(AnnotatedTypeMirror.AnnotatedExecutableType t, Void } } + @Override + protected CalledMethodsAnalysis createFlowAnalysis() { + return new CalledMethodsAnalysis(checker, this); + } + /** * Returns the annotation type mirror for the type of {@code expressionTree} with default * annotations applied. As types relevant to Called Methods checking are rarely used inside @@ -381,37 +385,36 @@ public Void visitExecutable(AnnotatedTypeMirror.AnnotatedExecutableType t, Void } @Override - public List getPostconditionAnnotation( - VariableElement elt, AnnotatedTypeMirror fieldAnnos, List preconds) { - AnnotationMirror cmAnno = - AnnotationUtils.getAnnotationByName( - fieldAnnos.getAnnotations(), - "org.checkerframework.checker.calledmethods.qual.CalledMethods"); - if (cmAnno != null) { + protected @Nullable AnnotationMirror createRequiresOrEnsuresQualifier( + String expression, + AnnotationMirror qualifier, + AnnotatedTypeMirror declaredType, + Analysis.BeforeOrAfter preOrPost, + @Nullable List preconds) { + if (preOrPost == BeforeOrAfter.AFTER && isAccumulatorAnnotation(qualifier)) { List calledMethods = - AnnotationUtils.getElementValueArray(cmAnno, calledMethodsValueElement, String.class); + AnnotationUtils.getElementValueArray(qualifier, calledMethodsValueElement, String.class); if (!calledMethods.isEmpty()) { - return ensuresCMAnno(elt, calledMethods); + return ensuresCMAnno(expression, calledMethods); } } - return Collections.emptyList(); + return super.createRequiresOrEnsuresQualifier( + expression, qualifier, declaredType, preOrPost, preconds); } /** - * Returns a {@code @EnsuresCalledMethods("...")} annotation for the given field. + * Returns a {@code @EnsuresCalledMethods("...")} annotation for the given expression. * - * @param fieldElement a field - * @param calledMethods the methods that were definitely called on the field - * @return a {@code @EnsuresCalledMethods("...")} annotation for the given field + * @param expression the expression to put in the value field of the EnsuresCalledMethods + * annotation + * @param calledMethods the methods that were definitely called on the expression + * @return a {@code @EnsuresCalledMethods("...")} annotation for the given expression */ - private List ensuresCMAnno( - VariableElement fieldElement, List calledMethods) { + private AnnotationMirror ensuresCMAnno(String expression, List calledMethods) { AnnotationBuilder builder = new AnnotationBuilder(processingEnv, EnsuresCalledMethods.class); - String receiver = JavaExpression.getImplicitReceiver(fieldElement).toString(); - String expression = receiver + "." + fieldElement.getSimpleName(); builder.setValue("value", new String[] {expression}); builder.setValue("methods", calledMethods.toArray(new String[0])); AnnotationMirror am = builder.build(); - return Collections.singletonList(am); + return am; } } diff --git a/checker/src/main/java/org/checkerframework/checker/calledmethods/CalledMethodsTransfer.java b/checker/src/main/java/org/checkerframework/checker/calledmethods/CalledMethodsTransfer.java index 0d4fe188ee..defb2a27f0 100644 --- a/checker/src/main/java/org/checkerframework/checker/calledmethods/CalledMethodsTransfer.java +++ b/checker/src/main/java/org/checkerframework/checker/calledmethods/CalledMethodsTransfer.java @@ -22,7 +22,6 @@ import org.checkerframework.dataflow.cfg.node.Node; import org.checkerframework.dataflow.expression.JavaExpression; import org.checkerframework.framework.flow.CFAbstractStore; -import org.checkerframework.framework.flow.CFAnalysis; import org.checkerframework.framework.flow.CFStore; import org.checkerframework.framework.flow.CFValue; import org.checkerframework.framework.type.AnnotatedTypeMirror; @@ -56,7 +55,7 @@ public class CalledMethodsTransfer extends AccumulationTransfer { * * @param analysis the analysis */ - public CalledMethodsTransfer(final CFAnalysis analysis) { + public CalledMethodsTransfer(final CalledMethodsAnalysis analysis) { super(analysis); calledMethodsValueElement = ((CalledMethodsAnnotatedTypeFactory) atypeFactory).calledMethodsValueElement; diff --git a/checker/src/main/java/org/checkerframework/checker/calledmethods/builder/AutoValueSupport.java b/checker/src/main/java/org/checkerframework/checker/calledmethods/builder/AutoValueSupport.java index 4e03dcf2a6..98902b8e9f 100644 --- a/checker/src/main/java/org/checkerframework/checker/calledmethods/builder/AutoValueSupport.java +++ b/checker/src/main/java/org/checkerframework/checker/calledmethods/builder/AutoValueSupport.java @@ -24,10 +24,10 @@ import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedExecutableType; import org.checkerframework.framework.util.AnnotatedTypes; import org.checkerframework.javacutil.AnnotationUtils; -import org.checkerframework.javacutil.BugInCF; import org.checkerframework.javacutil.ElementUtils; import org.checkerframework.javacutil.TreeUtils; import org.checkerframework.javacutil.TypesUtils; +import org.checkerframework.javacutil.UserError; import org.plumelib.util.ArraysPlume; /** @@ -80,7 +80,7 @@ builderElement, getAutoValuePackageName() + ".AutoValue.Builder")) { Element classContainingBuilderElement = builderElement.getEnclosingElement(); if (!ElementUtils.hasAnnotation( classContainingBuilderElement, getAutoValuePackageName() + ".AutoValue")) { - throw new BugInCF( + throw new UserError( "class " + classContainingBuilderElement.getSimpleName() + " is missing @AutoValue annotation"); diff --git a/checker/src/main/java/org/checkerframework/checker/fenum/FenumAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/fenum/FenumAnnotatedTypeFactory.java index 1c0c68d4e7..110fa429f8 100644 --- a/checker/src/main/java/org/checkerframework/checker/fenum/FenumAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/fenum/FenumAnnotatedTypeFactory.java @@ -20,7 +20,7 @@ import org.checkerframework.framework.util.QualifierKindHierarchy; import org.checkerframework.javacutil.AnnotationBuilder; import org.checkerframework.javacutil.AnnotationUtils; -import org.checkerframework.javacutil.BugInCF; +import org.checkerframework.javacutil.TypeSystemError; import org.checkerframework.javacutil.UserError; import org.plumelib.reflection.Signatures; @@ -149,7 +149,7 @@ protected AnnotationMirror leastUpperBoundWithElements( } else if (qualifierKind2 == FENUM_KIND) { return a2; } - throw new BugInCF("Unexpected QualifierKinds %s %s", qualifierKind1, qualifierKind2); + throw new TypeSystemError("Unexpected QualifierKinds %s %s", qualifierKind1, qualifierKind2); } @Override @@ -166,7 +166,7 @@ protected AnnotationMirror greatestLowerBoundWithElements( } else if (qualifierKind2 == FENUM_KIND) { return a1; } - throw new BugInCF("Unexpected QualifierKinds %s %s", qualifierKind1, qualifierKind2); + throw new TypeSystemError("Unexpected QualifierKinds %s %s", qualifierKind1, qualifierKind2); } } } diff --git a/checker/src/main/java/org/checkerframework/checker/fenum/FenumVisitor.java b/checker/src/main/java/org/checkerframework/checker/fenum/FenumVisitor.java index d986ccefb1..5b4b9920f8 100644 --- a/checker/src/main/java/org/checkerframework/checker/fenum/FenumVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/fenum/FenumVisitor.java @@ -7,6 +7,7 @@ import com.sun.source.tree.SwitchTree; import com.sun.source.tree.Tree; import java.util.Collections; +import java.util.List; import java.util.Set; import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.ExecutableElement; @@ -49,8 +50,9 @@ public Void visitSwitch(SwitchTree node, Void p) { AnnotatedTypeMirror exprType = atypeFactory.getAnnotatedType(expr); for (CaseTree caseExpr : node.getCases()) { - ExpressionTree realCaseExpr = caseExpr.getExpression(); - if (realCaseExpr != null) { + List realCaseExprs = TreeUtils.caseTreeGetExpressions(caseExpr); + // Check all the case options against the switch expression type: + for (ExpressionTree realCaseExpr : realCaseExprs) { AnnotatedTypeMirror caseType = atypeFactory.getAnnotatedType(realCaseExpr); // There is currently no "switch" message key, so it is treated diff --git a/checker/src/main/java/org/checkerframework/checker/formatter/FormatterAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/formatter/FormatterAnnotatedTypeFactory.java index d33c2ff36c..c9aee4b895 100644 --- a/checker/src/main/java/org/checkerframework/checker/formatter/FormatterAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/formatter/FormatterAnnotatedTypeFactory.java @@ -25,7 +25,7 @@ import org.checkerframework.framework.util.QualifierKind; import org.checkerframework.javacutil.AnnotationBuilder; import org.checkerframework.javacutil.AnnotationUtils; -import org.checkerframework.javacutil.BugInCF; +import org.checkerframework.javacutil.TypeSystemError; import scenelib.annotations.Annotation; import scenelib.annotations.el.AField; import scenelib.annotations.el.AMethod; @@ -218,7 +218,7 @@ protected boolean isSubtypeWithElements( } else if (subKind == INVALIDFORMAT_KIND && superKind == INVALIDFORMAT_KIND) { return true; } - throw new BugInCF("Unexpected kinds: %s %s", subKind, superKind); + throw new TypeSystemError("Unexpected kinds: %s %s", subKind, superKind); } @Override diff --git a/checker/src/main/java/org/checkerframework/checker/formatter/FormatterTreeUtil.java b/checker/src/main/java/org/checkerframework/checker/formatter/FormatterTreeUtil.java index c8816ecefe..8067d4a4d0 100644 --- a/checker/src/main/java/org/checkerframework/checker/formatter/FormatterTreeUtil.java +++ b/checker/src/main/java/org/checkerframework/checker/formatter/FormatterTreeUtil.java @@ -14,7 +14,7 @@ import javax.lang.model.type.NullType; import javax.lang.model.type.TypeKind; import javax.lang.model.type.TypeMirror; -import javax.lang.model.util.SimpleTypeVisitor7; +import javax.lang.model.util.SimpleTypeVisitor8; import org.checkerframework.checker.compilermsgs.qual.CompilerMessageKey; import org.checkerframework.checker.formatter.qual.ConversionCategory; import org.checkerframework.checker.formatter.qual.Format; @@ -274,7 +274,7 @@ public final Result getInvocationType() { // figure out if argType is an array type = argType.accept( - new SimpleTypeVisitor7>() { + new SimpleTypeVisitor8>() { @Override protected InvocationType defaultAction(TypeMirror e, Class p) { // not an array @@ -384,7 +384,7 @@ public final boolean isArgumentNull(TypeMirror type) { // is it the null literal return type.accept( - new SimpleTypeVisitor7>() { + new SimpleTypeVisitor8>() { @Override protected Boolean defaultAction(TypeMirror e, Class p) { // it's not the null literal diff --git a/checker/src/main/java/org/checkerframework/checker/formatter/FormatterVisitor.java b/checker/src/main/java/org/checkerframework/checker/formatter/FormatterVisitor.java index c2d8425ab6..827230e554 100644 --- a/checker/src/main/java/org/checkerframework/checker/formatter/FormatterVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/formatter/FormatterVisitor.java @@ -24,11 +24,11 @@ import org.checkerframework.common.wholeprograminference.WholeProgramInference; import org.checkerframework.framework.type.AnnotatedTypeMirror; import org.checkerframework.javacutil.AnnotationUtils; -import org.checkerframework.javacutil.BugInCF; import org.checkerframework.javacutil.ElementUtils; import org.checkerframework.javacutil.TreePathUtil; import org.checkerframework.javacutil.TreeUtils; import org.checkerframework.javacutil.TypesUtils; +import org.checkerframework.javacutil.UserError; /** * Whenever a format method invocation is found in the syntax tree, checks are performed as @@ -208,7 +208,7 @@ private boolean forwardsArguments( ExecutableElement calledMethodElement = TreeUtils.elementFromUse(invocationTree); int callIndex = formatStringIndex(calledMethodElement); if (callIndex == -1) { - throw new BugInCF( + throw new UserError( "Method " + calledMethodElement + " is annotated @FormatMethod but has no String formal parameter"); diff --git a/checker/src/main/java/org/checkerframework/checker/guieffect/GuiEffectTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/guieffect/GuiEffectTypeFactory.java index 3522d0f3fd..4553ff3007 100644 --- a/checker/src/main/java/org/checkerframework/checker/guieffect/GuiEffectTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/guieffect/GuiEffectTypeFactory.java @@ -36,9 +36,9 @@ import org.checkerframework.framework.type.treeannotator.TreeAnnotator; import org.checkerframework.framework.util.AnnotatedTypes; import org.checkerframework.javacutil.AnnotationBuilder; -import org.checkerframework.javacutil.BugInCF; import org.checkerframework.javacutil.ElementUtils; import org.checkerframework.javacutil.TreeUtils; +import org.checkerframework.javacutil.TypeSystemError; import org.checkerframework.javacutil.TypesUtils; /** Annotated type factory for the GUI Effect Checker. */ @@ -307,7 +307,7 @@ public Effect getComputedEffectAtCallsite( } srcType = callerReceiver; } else { - throw new BugInCF("Unexpected getMethodSelect() kind at callsite " + node); + throw new TypeSystemError("Unexpected getMethodSelect() kind at callsite " + node); } // Instantiate type-polymorphic effects diff --git a/checker/src/main/java/org/checkerframework/checker/guieffect/GuiEffectVisitor.java b/checker/src/main/java/org/checkerframework/checker/guieffect/GuiEffectVisitor.java index 306c58d6ac..81638386f4 100644 --- a/checker/src/main/java/org/checkerframework/checker/guieffect/GuiEffectVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/guieffect/GuiEffectVisitor.java @@ -28,6 +28,7 @@ import org.checkerframework.checker.guieffect.qual.SafeEffect; import org.checkerframework.checker.guieffect.qual.UI; import org.checkerframework.checker.guieffect.qual.UIEffect; +import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.common.basetype.BaseTypeChecker; import org.checkerframework.common.basetype.BaseTypeVisitor; import org.checkerframework.framework.type.AnnotatedTypeFactory.ParameterizedExecutableType; @@ -37,14 +38,18 @@ import org.checkerframework.framework.util.AnnotatedTypes; import org.checkerframework.javacutil.AnnotationBuilder; import org.checkerframework.javacutil.ElementUtils; -import org.checkerframework.javacutil.Pair; import org.checkerframework.javacutil.TreePathUtil; import org.checkerframework.javacutil.TreeUtils; import org.checkerframework.javacutil.TypesUtils; /** Require that only UI code invokes code with the UI effect. */ public class GuiEffectVisitor extends BaseTypeVisitor { + /** The type of the class currently being visited. */ + private @Nullable AnnotatedDeclaredType classType = null; + /** The receiver type of the enclosing method tree. */ + private @Nullable AnnotatedDeclaredType receiverType = null; + /** Whether or not to display debugging information. */ protected final boolean debugSpew; // effStack and currentMethods should always be the same size. @@ -229,7 +234,7 @@ public Void visitLambdaExpression(LambdaExpressionTree node, Void p) { // involving it. if (atypeFactory.isDirectlyMarkedUIThroughInference(node)) { // Backtrack path to the lambda expression itself - TreePath path = visitorState.getPath(); + TreePath path = getCurrentPath(); while (path.getLeaf() != node) { assert path.getLeaf().getKind() != Tree.Kind.COMPILATION_UNIT; path = path.getParentPath(); @@ -280,8 +285,7 @@ public Void visitMethodInvocation(MethodInvocationTree node, Void p) { System.err.println("callerTree found: " + callerTree.getKind()); } - Effect targetEffect = - atypeFactory.getComputedEffectAtCallsite(node, visitorState.getMethodReceiver(), methodElt); + Effect targetEffect = atypeFactory.getComputedEffectAtCallsite(node, receiverType, methodElt); Effect callerEffect = null; if (callerTree.getKind() == Tree.Kind.METHOD) { @@ -291,7 +295,7 @@ public Void visitMethodInvocation(MethodInvocationTree node, Void p) { } callerEffect = atypeFactory.getDeclaredEffect(callerElt); - final DeclaredType callerReceiverType = this.visitorState.getClassType().getUnderlyingType(); + final DeclaredType callerReceiverType = classType.getUnderlyingType(); assert callerReceiverType != null; final TypeElement callerReceiverElt = (TypeElement) callerReceiverType.asElement(); // Note: All these checks should be fast in the common case, but happen for every method call @@ -363,6 +367,10 @@ public Void visitMethodInvocation(MethodInvocationTree node, Void p) { @Override public Void visitMethod(MethodTree node, Void p) { + AnnotatedExecutableType methodType = atypeFactory.getAnnotatedType(node).deepCopy(); + AnnotatedDeclaredType previousReceiverType = receiverType; + receiverType = methodType.getReceiverType(); + // TODO: If the type we're in is a polymorphic (over effect qualifiers) type, the receiver must // be @PolyUI. Otherwise a "non-polymorphic" method of a polymorphic type could be called on a // UI instance, which then gets a Safe reference to itself (unsound!) that it can then pass off @@ -437,6 +445,7 @@ public Void visitMethod(MethodTree node, Void p) { Void ret = super.visitMethod(node, p); currentMethods.removeFirst(); effStack.removeFirst(); + receiverType = previousReceiverType; return ret; } @@ -448,12 +457,12 @@ public Void visitNewClass(NewClassTree node, Void p) { // assignments involving it. if (atypeFactory.isDirectlyMarkedUIThroughInference(node)) { // Backtrack path to the new class expression itself - TreePath path = visitorState.getPath(); + TreePath path = getCurrentPath(); while (path.getLeaf() != node) { assert path.getLeaf().getKind() != Tree.Kind.COMPILATION_UNIT; path = path.getParentPath(); } - scanUp(visitorState.getPath().getParentPath()); + scanUp(getCurrentPath().getParentPath()); } return v; } @@ -531,18 +540,11 @@ private void scanUp(TreePath path) { } if (ret != null) { - Pair preAssignmentContext = - visitorState.getAssignmentContext(); - try { - visitorState.setAssignmentContext(Pair.of((Tree) returnTree, ret)); - commonAssignmentCheck( - ret, - atypeFactory.getAnnotatedType(returnTree.getExpression()), - returnTree.getExpression(), - "return"); - } finally { - visitorState.setAssignmentContext(preAssignmentContext); - } + commonAssignmentCheck( + ret, + atypeFactory.getAnnotatedType(returnTree.getExpression()), + returnTree.getExpression(), + "return"); } } break; @@ -583,4 +585,18 @@ private void scanUp(TreePath path) { // currentMethods.removeFirst(); // effStack.removeFirst(); // } + + @Override + public void processClassTree(ClassTree classTree) { + AnnotatedDeclaredType previousClassType = classType; + AnnotatedDeclaredType previousReceiverType = receiverType; + receiverType = null; + classType = atypeFactory.getAnnotatedType(TreeUtils.elementFromDeclaration(classTree)); + try { + super.processClassTree(classTree); + } finally { + classType = previousClassType; + receiverType = previousReceiverType; + } + } } diff --git a/checker/src/main/java/org/checkerframework/checker/i18nformatter/I18nFormatterAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/i18nformatter/I18nFormatterAnnotatedTypeFactory.java index 50f928571b..efd0a2c171 100644 --- a/checker/src/main/java/org/checkerframework/checker/i18nformatter/I18nFormatterAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/i18nformatter/I18nFormatterAnnotatedTypeFactory.java @@ -32,7 +32,7 @@ import org.checkerframework.framework.util.QualifierKind; import org.checkerframework.javacutil.AnnotationBuilder; import org.checkerframework.javacutil.AnnotationUtils; -import org.checkerframework.javacutil.BugInCF; +import org.checkerframework.javacutil.TypeSystemError; import org.plumelib.reflection.Signatures; /** @@ -257,7 +257,7 @@ protected boolean isSubtypeWithElements( treeUtil.getI18nInvalidFormatValue(subAnno), treeUtil.getI18nInvalidFormatValue(superAnno)); } - throw new BugInCF("Unexpected QualifierKinds: %s %s", subKind, superKind); + throw new TypeSystemError("Unexpected QualifierKinds: %s %s", subKind, superKind); } @Override diff --git a/checker/src/main/java/org/checkerframework/checker/i18nformatter/I18nFormatterTreeUtil.java b/checker/src/main/java/org/checkerframework/checker/i18nformatter/I18nFormatterTreeUtil.java index fd91640ec5..297e9371da 100644 --- a/checker/src/main/java/org/checkerframework/checker/i18nformatter/I18nFormatterTreeUtil.java +++ b/checker/src/main/java/org/checkerframework/checker/i18nformatter/I18nFormatterTreeUtil.java @@ -18,8 +18,8 @@ import javax.lang.model.type.PrimitiveType; import javax.lang.model.type.TypeKind; import javax.lang.model.type.TypeMirror; -import javax.lang.model.util.SimpleElementVisitor7; -import javax.lang.model.util.SimpleTypeVisitor7; +import javax.lang.model.util.SimpleElementVisitor8; +import javax.lang.model.util.SimpleTypeVisitor8; import org.checkerframework.checker.compilermsgs.qual.CompilerMessageKey; import org.checkerframework.checker.formatter.FormatterTreeUtil.InvocationType; import org.checkerframework.checker.formatter.FormatterTreeUtil.Result; @@ -425,7 +425,7 @@ public final Result getInvocationType() { // figure out if argType is an array type = argType.accept( - new SimpleTypeVisitor7>() { + new SimpleTypeVisitor8>() { @Override protected InvocationType defaultAction(TypeMirror e, Class p) { // not an array @@ -513,7 +513,7 @@ public boolean isValidParameter(I18nConversionCategory formatCat, TypeMirror par /** Converts a TypeMirror to a Class. */ private static class TypeMirrorToClassVisitor - extends SimpleTypeVisitor7, Class> { + extends SimpleTypeVisitor8, Class> { @Override public Class visitPrimitive(PrimitiveType t, Class v) { switch (t.getKind()) { @@ -542,7 +542,7 @@ public Class visitPrimitive(PrimitiveType t, Class v) { public Class visitDeclared(DeclaredType dt, Class v) { return dt.asElement() .accept( - new SimpleElementVisitor7, Class>() { + new SimpleElementVisitor8, Class>() { @Override public Class visitType(TypeElement e, Class v) { try { diff --git a/checker/src/main/java/org/checkerframework/checker/index/IndexRefinementInfo.java b/checker/src/main/java/org/checkerframework/checker/index/IndexRefinementInfo.java index bb7a076b92..a180453467 100644 --- a/checker/src/main/java/org/checkerframework/checker/index/IndexRefinementInfo.java +++ b/checker/src/main/java/org/checkerframework/checker/index/IndexRefinementInfo.java @@ -10,7 +10,7 @@ import org.checkerframework.framework.flow.CFStore; import org.checkerframework.framework.flow.CFValue; import org.checkerframework.framework.type.QualifierHierarchy; -import org.checkerframework.javacutil.BugInCF; +import org.checkerframework.javacutil.TypeSystemError; /** * This struct contains all of the information that the refinement functions need. It's called by @@ -65,7 +65,7 @@ public IndexRefinementInfo( private static AnnotationMirror getAnno(Set set, QualifierHierarchy hierarchy) { Set tops = hierarchy.getTopAnnotations(); if (tops.size() != 1) { - throw new BugInCF( + throw new TypeSystemError( "%s: Found %d tops, but expected one.%nFound: %s", IndexRefinementInfo.class, tops.size(), tops); } diff --git a/checker/src/main/java/org/checkerframework/checker/index/searchindex/SearchIndexAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/index/searchindex/SearchIndexAnnotatedTypeFactory.java index cc4983cd4b..dd2fbcf0c0 100644 --- a/checker/src/main/java/org/checkerframework/checker/index/searchindex/SearchIndexAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/index/searchindex/SearchIndexAnnotatedTypeFactory.java @@ -23,8 +23,8 @@ import org.checkerframework.framework.type.QualifierHierarchy; import org.checkerframework.javacutil.AnnotationBuilder; import org.checkerframework.javacutil.AnnotationUtils; -import org.checkerframework.javacutil.BugInCF; import org.checkerframework.javacutil.TreeUtils; +import org.checkerframework.javacutil.TypeSystemError; /** * The Search Index Checker is used to help type the results of calls to the JDK's binary search @@ -92,7 +92,7 @@ private List getValueElement(AnnotationMirror am) { } else if (areSameByClass(am, SearchIndexFor.class)) { return AnnotationUtils.getElementValueArray(am, searchIndexForValueElement, String.class); } else { - throw new BugInCF("indexForValue(%s)", am); + throw new TypeSystemError("indexForValue(%s)", am); } } diff --git a/checker/src/main/java/org/checkerframework/checker/index/upperbound/UBQualifier.java b/checker/src/main/java/org/checkerframework/checker/index/upperbound/UBQualifier.java index f9e40e3b97..5636a672e3 100644 --- a/checker/src/main/java/org/checkerframework/checker/index/upperbound/UBQualifier.java +++ b/checker/src/main/java/org/checkerframework/checker/index/upperbound/UBQualifier.java @@ -20,8 +20,8 @@ import org.checkerframework.framework.type.AnnotatedTypeMirror; import org.checkerframework.javacutil.AnnotationBuilder; import org.checkerframework.javacutil.AnnotationUtils; -import org.checkerframework.javacutil.BugInCF; import org.checkerframework.javacutil.Pair; +import org.checkerframework.javacutil.TypeSystemError; import org.plumelib.util.CollectionsPlume; /** @@ -78,7 +78,7 @@ public static UBQualifier createUBQualifier( // TODO: Ignores offset. Should we check that offset is not set? return PolyQualifier.POLY; default: - throw new BugInCF("createUBQualifier(%s, %s, ...)", am, offset); + throw new TypeSystemError("createUBQualifier(%s, %s, ...)", am, offset); } } @@ -689,7 +689,7 @@ private AnnotationMirror convertToAnnotation( builder.setValue("value", sequences); builder.setValue("offset", offsets); } else { - throw new BugInCF("What annoClass? " + annoClass); + throw new TypeSystemError("What annoClass? " + annoClass); } return builder.build(); } diff --git a/checker/src/main/java/org/checkerframework/checker/index/upperbound/UpperBoundAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/index/upperbound/UpperBoundAnnotatedTypeFactory.java index 5bf33afcdd..2b35a17c63 100644 --- a/checker/src/main/java/org/checkerframework/checker/index/upperbound/UpperBoundAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/index/upperbound/UpperBoundAnnotatedTypeFactory.java @@ -75,9 +75,9 @@ import org.checkerframework.framework.util.dependenttypes.DependentTypesHelper; import org.checkerframework.javacutil.AnnotationBuilder; import org.checkerframework.javacutil.AnnotationUtils; -import org.checkerframework.javacutil.BugInCF; import org.checkerframework.javacutil.Pair; import org.checkerframework.javacutil.TreeUtils; +import org.checkerframework.javacutil.TypeSystemError; /** * Implements the introduction rules for the Upper Bound Checker. @@ -334,18 +334,32 @@ public boolean isRandomNextInt(Tree methodTree) { return imf.isRandomNextInt(methodTree, processingEnv); } + /** + * Creates a new @LTLengthOf annotation. + * + * @param names the arguments to @LTLengthOf + * @return a new @LTLengthOf annotation with the given arguments + */ AnnotationMirror createLTLengthOfAnnotation(String... names) { if (names == null || names.length == 0) { - throw new BugInCF("createLTLengthOfAnnotation: bad argument %s", Arrays.toString(names)); + throw new TypeSystemError( + "createLTLengthOfAnnotation: bad argument %s", Arrays.toString(names)); } AnnotationBuilder builder = new AnnotationBuilder(getProcessingEnv(), LTLengthOf.class); builder.setValue("value", names); return builder.build(); } + /** + * Creates a new @LTEqLengthOf annotation. + * + * @param names the arguments to @LTEqLengthOf + * @return a new @LTEqLengthOf annotation with the given arguments + */ AnnotationMirror createLTEqLengthOfAnnotation(String... names) { if (names == null || names.length == 0) { - throw new BugInCF("createLTEqLengthOfAnnotation: bad argument %s", Arrays.toString(names)); + throw new TypeSystemError( + "createLTEqLengthOfAnnotation: bad argument %s", Arrays.toString(names)); } AnnotationBuilder builder = new AnnotationBuilder(getProcessingEnv(), LTEqLengthOf.class); builder.setValue("value", names); diff --git a/checker/src/main/java/org/checkerframework/checker/index/upperbound/UpperBoundTransfer.java b/checker/src/main/java/org/checkerframework/checker/index/upperbound/UpperBoundTransfer.java index 1be086ea11..919f930c69 100644 --- a/checker/src/main/java/org/checkerframework/checker/index/upperbound/UpperBoundTransfer.java +++ b/checker/src/main/java/org/checkerframework/checker/index/upperbound/UpperBoundTransfer.java @@ -824,10 +824,12 @@ public TransferResult visitCase( // Refines subtrahend in the switch expression // TODO: This cannot be done in strengthenAnnotationOfEqualTo, because that does not provide // transfer input. - Node caseNode = n.getCaseOperand(); - AssignmentNode assign = (AssignmentNode) n.getSwitchOperand(); + List caseNodes = n.getCaseOperands(); + AssignmentNode assign = n.getSwitchOperand(); Node switchNode = assign.getExpression(); - refineSubtrahendWithOffset(switchNode, caseNode, false, in, result.getThenStore()); + for (Node caseNode : caseNodes) { + refineSubtrahendWithOffset(switchNode, caseNode, false, in, result.getThenStore()); + } return result; } diff --git a/checker/src/main/java/org/checkerframework/checker/initialization/InitializationVisitor.java b/checker/src/main/java/org/checkerframework/checker/initialization/InitializationVisitor.java index f8c168b519..5894b65166 100644 --- a/checker/src/main/java/org/checkerframework/checker/initialization/InitializationVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/initialization/InitializationVisitor.java @@ -20,7 +20,6 @@ import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.Element; import javax.lang.model.element.ExecutableElement; -import javax.lang.model.element.VariableElement; import org.checkerframework.checker.compilermsgs.qual.CompilerMessageKey; import org.checkerframework.checker.nullness.NullnessChecker; import org.checkerframework.common.basetype.BaseTypeChecker; @@ -31,6 +30,7 @@ import org.checkerframework.dataflow.expression.JavaExpression; import org.checkerframework.dataflow.expression.LocalVariable; import org.checkerframework.dataflow.expression.ThisReference; +import org.checkerframework.framework.flow.CFAbstractAnalysis.FieldInitialValue; import org.checkerframework.framework.flow.CFAbstractStore; import org.checkerframework.framework.flow.CFAbstractValue; import org.checkerframework.framework.type.AnnotatedTypeMirror; @@ -273,8 +273,11 @@ public void processClassTree(ClassTree node) { Store store = atypeFactory.getRegularExitStore(block); // Add field values for fields with an initializer. - for (Pair t : store.getAnalysis().getFieldValues()) { - store.addInitializedField(t.first); + for (FieldInitialValue fieldInitialValue : + store.getAnalysis().getFieldInitialValues()) { + if (fieldInitialValue.initializer != null) { + store.addInitializedField(fieldInitialValue.fieldDecl.getField()); + } } final List init = atypeFactory.getInitializedInvariantFields(store, getCurrentPath()); @@ -285,15 +288,22 @@ public void processClassTree(ClassTree node) { super.processClassTree(node); // Warn about uninitialized static fields. - if (node.getKind() == Tree.Kind.CLASS) { + Tree.Kind nodeKind = node.getKind(); + // Skip interfaces (and annotations, which are interfaces). In an interface, every static field + // must be initialized. Java forbids uninitialized variables and static initalizer blocks. + if (nodeKind != Tree.Kind.INTERFACE && nodeKind != Tree.Kind.ANNOTATION_TYPE) { boolean isStatic = true; // See GenericAnnotatedTypeFactory.performFlowAnalysis for why we use // the regular exit store of the class here. Store store = atypeFactory.getRegularExitStore(node); // Add field values for fields with an initializer. - for (Pair t : store.getAnalysis().getFieldValues()) { - store.addInitializedField(t.first); + for (FieldInitialValue fieldInitialValue : + store.getAnalysis().getFieldInitialValues()) { + if (fieldInitialValue.initializer != null) { + store.addInitializedField(fieldInitialValue.fieldDecl.getField()); + } } + List receiverAnnotations = Collections.emptyList(); checkFieldsInitialized(node, isStatic, store, receiverAnnotations); } @@ -369,19 +379,25 @@ protected void checkFieldsInitialized( return; } + // Compact canonical record constructors do not generate visible assignments in the source, + // but by definition they assign to all the record's fields so we don't need to + // check for uninitialized fields in them: + if (node.getKind() == Tree.Kind.METHOD + && TreeUtils.isCompactCanonicalRecordConstructor((MethodTree) node)) { + return; + } + Pair, List> uninitializedFields = atypeFactory.getUninitializedFields( store, getCurrentPath(), staticFields, receiverAnnotations); List violatingFields = uninitializedFields.first; List nonviolatingFields = uninitializedFields.second; + // Remove fields that have already been initialized by an initializer block. if (staticFields) { - // TODO: Why is nothing done for static fields? - // Do we need the following? - // violatingFields.removeAll(store.initializedFields); + violatingFields.removeAll(initializedFields); + nonviolatingFields.removeAll(initializedFields); } else { - // remove fields that have already been initialized by an - // initializer block violatingFields.removeAll(initializedFields); nonviolatingFields.removeAll(initializedFields); } diff --git a/checker/src/main/java/org/checkerframework/checker/interning/InterningVisitor.java b/checker/src/main/java/org/checkerframework/checker/interning/InterningVisitor.java index 408aedd99b..297c407314 100644 --- a/checker/src/main/java/org/checkerframework/checker/interning/InterningVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/interning/InterningVisitor.java @@ -517,8 +517,7 @@ private boolean suppressInsideComparison(final BinaryTree node) { return false; } - ExecutableElement enclosingMethod = - TreeUtils.elementFromDeclaration(visitorState.getMethodTree()); + ExecutableElement enclosingMethod = TreeUtils.elementFromDeclaration(methodTree); assert enclosingMethod != null; final Element lhs = TreeUtils.elementFromUse((IdentifierTree) left); diff --git a/checker/src/main/java/org/checkerframework/checker/lock/LockAnalysis.java b/checker/src/main/java/org/checkerframework/checker/lock/LockAnalysis.java index b1ee377519..50e386e6af 100644 --- a/checker/src/main/java/org/checkerframework/checker/lock/LockAnalysis.java +++ b/checker/src/main/java/org/checkerframework/checker/lock/LockAnalysis.java @@ -1,15 +1,12 @@ package org.checkerframework.checker.lock; -import java.util.List; import java.util.Set; import javax.lang.model.element.AnnotationMirror; -import javax.lang.model.element.VariableElement; import javax.lang.model.type.TypeMirror; import org.checkerframework.common.basetype.BaseTypeChecker; import org.checkerframework.framework.flow.CFAbstractAnalysis; import org.checkerframework.framework.flow.CFStore; import org.checkerframework.framework.flow.CFValue; -import org.checkerframework.javacutil.Pair; /** * The analysis class for the lock type system. @@ -19,11 +16,14 @@ */ public class LockAnalysis extends CFAbstractAnalysis { - public LockAnalysis( - BaseTypeChecker checker, - LockAnnotatedTypeFactory factory, - List> fieldValues) { - super(checker, factory, fieldValues); + /** + * Creates a new {@link LockAnalysis}. + * + * @param checker the checker + * @param factory the factory + */ + public LockAnalysis(BaseTypeChecker checker, LockAnnotatedTypeFactory factory) { + super(checker, factory); } @Override diff --git a/checker/src/main/java/org/checkerframework/checker/lock/LockAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/lock/LockAnnotatedTypeFactory.java index 5e1c1dbd3c..552b0891e9 100644 --- a/checker/src/main/java/org/checkerframework/checker/lock/LockAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/lock/LockAnnotatedTypeFactory.java @@ -19,7 +19,6 @@ import javax.lang.model.element.Element; import javax.lang.model.element.ElementKind; import javax.lang.model.element.ExecutableElement; -import javax.lang.model.element.VariableElement; import javax.lang.model.util.Elements; import org.checkerframework.checker.lock.qual.EnsuresLockHeld; import org.checkerframework.checker.lock.qual.EnsuresLockHeldIf; @@ -61,10 +60,9 @@ import org.checkerframework.framework.util.dependenttypes.DependentTypesHelper; import org.checkerframework.javacutil.AnnotationBuilder; import org.checkerframework.javacutil.AnnotationUtils; -import org.checkerframework.javacutil.BugInCF; import org.checkerframework.javacutil.ElementUtils; -import org.checkerframework.javacutil.Pair; import org.checkerframework.javacutil.TreeUtils; +import org.checkerframework.javacutil.TypeSystemError; import org.plumelib.util.CollectionsPlume; /** @@ -265,8 +263,8 @@ protected QualifierHierarchy createQualifierHierarchy() { } @Override - protected LockAnalysis createFlowAnalysis(List> fieldValues) { - return new LockAnalysis(checker, this, fieldValues); + protected LockAnalysis createFlowAnalysis() { + return new LockAnalysis(checker, this); } @Override @@ -359,7 +357,7 @@ protected AnnotationMirror leastUpperBoundWithElements( } else if (qualifierKind2 == NEWOBJECT_KIND) { return a1; } - throw new BugInCF( + throw new TypeSystemError( "leastUpperBoundWithElements(%s, %s, %s, %s, %s)", a1, qualifierKind1, a2, qualifierKind2, lubKind); } @@ -396,7 +394,7 @@ protected AnnotationMirror greatestLowerBoundWithElements( } else if (qualifierKind2 == GUARDEDBYUNKNOWN_KIND) { return a1; } - throw new BugInCF( + throw new TypeSystemError( "greatestLowerBoundWithElements(%s, %s, %s, %s, %s)", a1, qualifierKind1, a2, qualifierKind2, glbKind); } diff --git a/checker/src/main/java/org/checkerframework/checker/lock/LockTreeAnnotator.java b/checker/src/main/java/org/checkerframework/checker/lock/LockTreeAnnotator.java index b05a1fe035..f05a6cf2b9 100644 --- a/checker/src/main/java/org/checkerframework/checker/lock/LockTreeAnnotator.java +++ b/checker/src/main/java/org/checkerframework/checker/lock/LockTreeAnnotator.java @@ -36,7 +36,12 @@ public Void visitBinary(BinaryTree node, AnnotatedTypeMirror type) { return super.visitBinary(node, type); } - /** Indicates that the result of the operation is a boolean value. */ + /** + * Indicates that the result of the operation is a boolean value. + * + * @param opKind the operation to check + * @return whether the result is boolean + */ private static boolean isBinaryComparisonOrInstanceOfOperator(Tree.Kind opKind) { switch (opKind) { case EQUAL_TO: diff --git a/checker/src/main/java/org/checkerframework/checker/lock/LockVisitor.java b/checker/src/main/java/org/checkerframework/checker/lock/LockVisitor.java index f8ad0b7f66..6abd8cac7d 100644 --- a/checker/src/main/java/org/checkerframework/checker/lock/LockVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/lock/LockVisitor.java @@ -57,10 +57,10 @@ import org.checkerframework.framework.util.StringToJavaExpression; import org.checkerframework.framework.util.dependenttypes.DependentTypesError; import org.checkerframework.javacutil.AnnotationUtils; -import org.checkerframework.javacutil.BugInCF; import org.checkerframework.javacutil.ElementUtils; import org.checkerframework.javacutil.TreePathUtil; import org.checkerframework.javacutil.TreeUtils; +import org.checkerframework.javacutil.TypeSystemError; import org.checkerframework.javacutil.TypesUtils; import org.plumelib.util.CollectionsPlume; @@ -71,11 +71,19 @@ * @checker_framework.manual #lock-checker Lock Checker */ public class LockVisitor extends BaseTypeVisitor { + /** The class of GuardedBy */ private final Class checkerGuardedByClass = GuardedBy.class; + /** The class of GuardSatisfied */ private final Class checkerGuardSatisfiedClass = GuardSatisfied.class; + /** A pattern for spotting self receiver */ protected static final Pattern SELF_RECEIVER_PATTERN = Pattern.compile("^(\\.(.*))?$"); + /** + * Constructs a {@link LockVisitor}. + * + * @param checker the type checker to use. + */ public LockVisitor(BaseTypeChecker checker) { super(checker); for (String checkerName : atypeFactory.getCheckerNames()) { @@ -1096,9 +1104,16 @@ private void checkLock(Tree tree, AnnotationMirror gbAnno) { checkLockOfThisOrTree(tree, false, gbAnno); } + /** + * Helper method tat checks the lock of either the implicit {@code this} or the given tree. + * + * @param tree a tree whose lock to check + * @param implicitThis true if checking the lock of the implicit {@code this} + * @param gbAnno a @GuardedBy annotation + */ private void checkLockOfThisOrTree(Tree tree, boolean implicitThis, AnnotationMirror gbAnno) { if (gbAnno == null) { - throw new BugInCF("LockVisitor.checkLock: gbAnno cannot be null"); + throw new TypeSystemError("LockVisitor.checkLock: gbAnno cannot be null"); } if (atypeFactory.areSameByClass(gbAnno, GuardedByUnknown.class) || atypeFactory.areSameByClass(gbAnno, GuardedByBottom.class)) { diff --git a/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallAnnotatedTypeFactory.java index fdf3f01f85..493a4bec6d 100644 --- a/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallAnnotatedTypeFactory.java @@ -46,9 +46,9 @@ import org.checkerframework.framework.type.typeannotator.TypeAnnotator; import org.checkerframework.javacutil.AnnotationBuilder; import org.checkerframework.javacutil.AnnotationUtils; -import org.checkerframework.javacutil.BugInCF; import org.checkerframework.javacutil.ElementUtils; import org.checkerframework.javacutil.TreeUtils; +import org.checkerframework.javacutil.TypeSystemError; /** * The annotated type factory for the Must Call Checker. Primarily responsible for the subtyping @@ -160,8 +160,7 @@ protected TypeAnnotator createTypeAnnotator() { * @return a MustCall annotation that does not have "close" as one of its values, but is otherwise * identical to anno */ - // Package private to permit usage from the visitor in the common assignment check. - /* package-private */ AnnotationMirror withoutClose(@Nullable AnnotationMirror anno) { + public AnnotationMirror withoutClose(@Nullable AnnotationMirror anno) { if (anno == null || AnnotationUtils.areSame(anno, BOTTOM)) { return BOTTOM; } else if (!AnnotationUtils.areSameByName( @@ -197,7 +196,7 @@ public void methodFromUsePreSubstitution(ExpressionTree tree, AnnotatedExecutabl } else if (tree instanceof MemberReferenceTree) { declaration = (ExecutableElement) TreeUtils.elementFromTree(tree); } else { - throw new BugInCF("unexpected type of method tree: " + tree.getKind()); + throw new TypeSystemError("unexpected type of method tree: " + tree.getKind()); } changeNonOwningParameterTypesToTop(declaration, type); super.methodFromUsePreSubstitution(tree, type); diff --git a/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallNoCreatesMustCallForChecker.java b/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallNoCreatesMustCallForChecker.java index 5f77be65d2..6affabc60f 100644 --- a/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallNoCreatesMustCallForChecker.java +++ b/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallNoCreatesMustCallForChecker.java @@ -2,6 +2,7 @@ import org.checkerframework.framework.qual.StubFiles; import org.checkerframework.framework.source.SupportedOptions; +import org.checkerframework.framework.source.SuppressWarningsPrefix; /** * This copy of the Must Call Checker is identical, except that it does not load the stub files that @@ -13,5 +14,12 @@ "JavaEE.astub", "Reflection.astub", }) +@SuppressWarningsPrefix({ + // Preferred checkername, so that warnings are suppressed regardless of the option passed. + "mustcall", + // Also supported, but will only suppress warnings from this checker (and not from the regular + // Must Call Checker). + "mustcallnocreatesmustcallfor" +}) @SupportedOptions({MustCallChecker.NO_CREATES_MUSTCALLFOR}) public class MustCallNoCreatesMustCallForChecker extends MustCallChecker {} diff --git a/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallTransfer.java b/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallTransfer.java index c094008187..5bfd91a148 100644 --- a/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallTransfer.java +++ b/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallTransfer.java @@ -178,7 +178,11 @@ public TransferResult visitObjectCreation( public TransferResult visitTernaryExpression( TernaryExpressionNode node, TransferInput input) { TransferResult result = super.visitTernaryExpression(node, input); - updateStoreWithTempVar(result, node); + if (!TypesUtils.isPrimitiveOrBoxed(node.getType())) { + // Add the synthetic variable created during CFG construction to the temporary + // variable map (rather than creating a redundant temp var) + atypeFactory.tempVars.put(node.getTree(), node.getTernaryExpressionVar()); + } return result; } diff --git a/checker/src/main/java/org/checkerframework/checker/nullness/KeyForAnalysis.java b/checker/src/main/java/org/checkerframework/checker/nullness/KeyForAnalysis.java index 980d7d40bd..49568e3586 100644 --- a/checker/src/main/java/org/checkerframework/checker/nullness/KeyForAnalysis.java +++ b/checker/src/main/java/org/checkerframework/checker/nullness/KeyForAnalysis.java @@ -1,31 +1,23 @@ package org.checkerframework.checker.nullness; -import java.util.List; import java.util.Set; import javax.lang.model.element.AnnotationMirror; -import javax.lang.model.element.VariableElement; import javax.lang.model.type.TypeMirror; import org.checkerframework.common.basetype.BaseTypeChecker; import org.checkerframework.framework.flow.CFAbstractAnalysis; import org.checkerframework.framework.flow.CFAbstractValue; -import org.checkerframework.javacutil.Pair; /** Boilerplate code to glue together all the parts the KeyFor dataflow classes. */ public class KeyForAnalysis extends CFAbstractAnalysis { - public KeyForAnalysis( - BaseTypeChecker checker, - KeyForAnnotatedTypeFactory factory, - List> fieldValues, - int maxCountBeforeWidening) { - super(checker, factory, fieldValues, maxCountBeforeWidening); - } - - public KeyForAnalysis( - BaseTypeChecker checker, - KeyForAnnotatedTypeFactory factory, - List> fieldValues) { - super(checker, factory, fieldValues); + /** + * Creates a new {@code KeyForAnalysis}. + * + * @param checker the checker + * @param factory the factory + */ + public KeyForAnalysis(BaseTypeChecker checker, KeyForAnnotatedTypeFactory factory) { + super(checker, factory); } @Override diff --git a/checker/src/main/java/org/checkerframework/checker/nullness/KeyForAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/nullness/KeyForAnnotatedTypeFactory.java index c9b0cbed29..3049678536 100644 --- a/checker/src/main/java/org/checkerframework/checker/nullness/KeyForAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/nullness/KeyForAnnotatedTypeFactory.java @@ -8,11 +8,9 @@ import java.util.Collection; import java.util.Collections; import java.util.LinkedHashSet; -import java.util.List; import java.util.Set; import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.ExecutableElement; -import javax.lang.model.element.VariableElement; import javax.lang.model.type.TypeKind; import org.checkerframework.checker.nullness.qual.KeyFor; import org.checkerframework.checker.nullness.qual.KeyForBottom; @@ -33,7 +31,6 @@ import org.checkerframework.framework.type.treeannotator.TreeAnnotator; import org.checkerframework.javacutil.AnnotationBuilder; import org.checkerframework.javacutil.AnnotationUtils; -import org.checkerframework.javacutil.Pair; import org.checkerframework.javacutil.TreeUtils; public class KeyForAnnotatedTypeFactory @@ -157,10 +154,9 @@ protected boolean isSubtype( } @Override - protected KeyForAnalysis createFlowAnalysis( - List> fieldValues) { + protected KeyForAnalysis createFlowAnalysis() { // Explicitly call the constructor instead of using reflection. - return new KeyForAnalysis(checker, this, fieldValues); + return new KeyForAnalysis(checker, this); } @Override diff --git a/checker/src/main/java/org/checkerframework/checker/nullness/KeyForPropagator.java b/checker/src/main/java/org/checkerframework/checker/nullness/KeyForPropagator.java index 00e1f4f0ec..4b66921942 100644 --- a/checker/src/main/java/org/checkerframework/checker/nullness/KeyForPropagator.java +++ b/checker/src/main/java/org/checkerframework/checker/nullness/KeyForPropagator.java @@ -1,7 +1,6 @@ package org.checkerframework.checker.nullness; import com.sun.source.tree.NewClassTree; -import com.sun.source.tree.Tree; import com.sun.source.util.TreePath; import java.util.List; import java.util.Set; @@ -157,8 +156,7 @@ public void propagateNewClassTree( NewClassTree newClassTree, AnnotatedTypeMirror type, KeyForAnnotatedTypeFactory atypeFactory) { - Pair context = atypeFactory.getVisitorState().getAssignmentContext(); - if (type.getKind() != TypeKind.DECLARED || context == null || context.first == null) { + if (type.getKind() != TypeKind.DECLARED) { return; } TreePath path = atypeFactory.getPath(newClassTree); diff --git a/checker/src/main/java/org/checkerframework/checker/nullness/KeyForValue.java b/checker/src/main/java/org/checkerframework/checker/nullness/KeyForValue.java index b886f20175..4d8287362e 100644 --- a/checker/src/main/java/org/checkerframework/checker/nullness/KeyForValue.java +++ b/checker/src/main/java/org/checkerframework/checker/nullness/KeyForValue.java @@ -19,14 +19,18 @@ * type. For example, * *
- * {@code Map map = ...;}
- * {@code  T method(T param) { }
- *   if (map.contains(param) {
  *     {@code @NonNull Object o = map.get(param);}
+ * 
+ * + *

+ * Map<T, Object> map = ...;
+ * <T> T method(T param) {
+ *   if (map.contains(param)) {
+ *     @NonNull Object o = map.get(param);
  *     return param;
  *   }
  * }
- * }
+ * * * Inside the if statement, {@code param} is a key for "map". This would normally be represented as * {@code @KeyFor("map") T}, but this is not a subtype of {@code T}, so the type cannot be refined. diff --git a/checker/src/main/java/org/checkerframework/checker/nullness/NullnessAnalysis.java b/checker/src/main/java/org/checkerframework/checker/nullness/NullnessAnalysis.java index cec0475335..492469105a 100644 --- a/checker/src/main/java/org/checkerframework/checker/nullness/NullnessAnalysis.java +++ b/checker/src/main/java/org/checkerframework/checker/nullness/NullnessAnalysis.java @@ -1,14 +1,11 @@ package org.checkerframework.checker.nullness; -import java.util.List; import java.util.Set; import javax.lang.model.element.AnnotationMirror; -import javax.lang.model.element.VariableElement; import javax.lang.model.type.TypeMirror; import org.checkerframework.common.basetype.BaseTypeChecker; import org.checkerframework.framework.flow.CFAbstractAnalysis; import org.checkerframework.framework.flow.CFAbstractValue; -import org.checkerframework.javacutil.Pair; /** * The analysis class for the non-null type system (serves as factory for the transfer function, @@ -17,11 +14,14 @@ public class NullnessAnalysis extends CFAbstractAnalysis { - public NullnessAnalysis( - BaseTypeChecker checker, - NullnessAnnotatedTypeFactory factory, - List> fieldValues) { - super(checker, factory, fieldValues); + /** + * Creates a new {@code NullnessAnalysis}. + * + * @param checker the checker + * @param factory the factory + */ + public NullnessAnalysis(BaseTypeChecker checker, NullnessAnnotatedTypeFactory factory) { + super(checker, factory); } @Override diff --git a/checker/src/main/java/org/checkerframework/checker/nullness/NullnessAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/nullness/NullnessAnnotatedTypeFactory.java index 0af4b2981a..4983d0fe76 100644 --- a/checker/src/main/java/org/checkerframework/checker/nullness/NullnessAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/nullness/NullnessAnnotatedTypeFactory.java @@ -45,7 +45,8 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; import org.checkerframework.checker.signature.qual.FullyQualifiedName; import org.checkerframework.common.basetype.BaseTypeChecker; -import org.checkerframework.dataflow.expression.JavaExpression; +import org.checkerframework.dataflow.analysis.Analysis; +import org.checkerframework.dataflow.analysis.Analysis.BeforeOrAfter; import org.checkerframework.framework.flow.CFAbstractAnalysis; import org.checkerframework.framework.type.AnnotatedTypeFactory; import org.checkerframework.framework.type.AnnotatedTypeFormatter; @@ -55,7 +56,7 @@ import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedExecutableType; import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedNoType; import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedPrimitiveType; -import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedWildcardType; +import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedTypeVariable; import org.checkerframework.framework.type.QualifierHierarchy; import org.checkerframework.framework.type.treeannotator.ListTreeAnnotator; import org.checkerframework.framework.type.treeannotator.LiteralTreeAnnotator; @@ -69,10 +70,10 @@ import org.checkerframework.framework.util.QualifierKind; import org.checkerframework.javacutil.AnnotationBuilder; import org.checkerframework.javacutil.AnnotationUtils; -import org.checkerframework.javacutil.BugInCF; import org.checkerframework.javacutil.Pair; import org.checkerframework.javacutil.TreePathUtil; import org.checkerframework.javacutil.TreeUtils; +import org.checkerframework.javacutil.TypeSystemError; import org.checkerframework.javacutil.TypesUtils; /** The annotated type factory for the nullness type-system. */ @@ -105,63 +106,112 @@ public class NullnessAnnotatedTypeFactory protected final Set> nullnessAnnos; // List is in alphabetical order. If you update it, also update - // ../../../../../../../../docs/manual/nullness-checker.tex . + // ../../../../../../../../docs/manual/nullness-checker.tex + // and make a pull request for variables NONNULL_ANNOTATIONS and BASE_COPYABLE_ANNOTATIONS in + // https://github.com/rzwitserloot/lombok/blob/master/src/core/lombok/core/handlers/HandlerUtil.java . /** Aliases for {@code @Nonnull}. */ private static final List<@FullyQualifiedName String> NONNULL_ALIASES = Arrays.asList( - // https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/annotation/NonNull.java + // https://developer.android.com/reference/androidx/annotation/NonNull "android.annotation.NonNull", - // https://android.googlesource.com/platform/frameworks/support/+/master/annotations/src/main/java/android/support/annotation/NonNull.java + // https://developer.android.com/reference/android/support/annotation/NonNull "android.support.annotation.NonNull", - // https://android.googlesource.com/platform/frameworks/support/+/master/annotations/src/main/java/androidx/annotation/NonNull.java + // https://android.googlesource.com/platform/tools/metalava/+/fcb3d99ad3fe17a266d066b28ceb6c526f7aa415/stub-annotations/src/main/java/android/support/annotation/RecentlyNonNull.java + "android.support.annotation.RecentlyNonNull", + // https://developer.android.com/reference/androidx/annotation/NonNull "androidx.annotation.NonNull", // https://android.googlesource.com/platform/tools/metalava/+/master/stub-annotations/src/main/java/androidx/annotation/RecentlyNonNull.java "androidx.annotation.RecentlyNonNull", + // https://android.googlesource.com/platform/sdk/+/66fcecc/common/src/com/android/annotations/NonNull.java + "com.android.annotations.NonNull", + // https://github.com/firebase/firebase-android-sdk/blob/master/firebase-database/src/main/java/com/google/firebase/database/annotations/NotNull.java + "com.google.firebase.database.annotations.NotNull", + // https://github.com/firebase/firebase-admin-java/blob/master/src/main/java/com/google/firebase/internal/NonNull.java + "com.google.firebase.internal.NonNull", + // https://github.com/mongodb/mongo-java-driver/blob/master/driver-core/src/main/com/mongodb/lang/NonNull.java + "com.mongodb.lang.NonNull", + // https://github.com/eclipse-ee4j/jaxb-istack-commons/blob/master/istack-commons/runtime/src/main/java/com/sun/istack/NotNull.java + "com.sun.istack.NotNull", + // https://github.com/openjdk/jdk8/blob/master/jaxws/src/share/jaxws_classes/com/sun/istack/internal/NotNull.java "com.sun.istack.internal.NotNull", + // https://github.com/pingidentity/ldapsdk/blob/master/src/com/unboundid/util/NotNull.java + "com.unboundid.util.NotNull", // http://findbugs.sourceforge.net/api/edu/umd/cs/findbugs/annotations/NonNull.html "edu.umd.cs.findbugs.annotations.NonNull", + // https://github.com/micrometer-metrics/micrometer/blob/main/micrometer-core/src/main/java/io/micrometer/core/lang/NonNull.java + "io.micrometer.core.lang.NonNull", // https://github.com/ReactiveX/RxJava/blob/2.x/src/main/java/io/reactivex/annotations/NonNull.java "io.reactivex.annotations.NonNull", // https://github.com/ReactiveX/RxJava/blob/3.x/src/main/java/io/reactivex/rxjava3/annotations/NonNull.java "io.reactivex.rxjava3.annotations.NonNull", - // https://jcp.org/en/jsr/detail?id=305 + // https://jcp.org/en/jsr/detail?id=305; no documentation at + // https://www.javadoc.io/doc/com.google.code.findbugs/jsr305/3.0.1/javax/annotation/Nonnull.html "javax.annotation.Nonnull", // https://javaee.github.io/javaee-spec/javadocs/javax/validation/constraints/NotNull.html "javax.validation.constraints.NotNull", + // https://android.googlesource.com/platform/libcore/+/master/luni/src/main/java/libcore/util/NonNull.java + "libcore.util.NonNull", // https://github.com/projectlombok/lombok/blob/master/src/core/lombok/NonNull.java "lombok.NonNull", - // https://search.maven.org/search?q=a:checker-compat-qual + // https://github.com/antlr/antlr4/blob/master/runtime/Java/src/org/antlr/v4/runtime/misc/NotNull.java + "org.antlr.v4.runtime.misc.NotNull", + // https://search.maven.org/artifact/org.checkerframework/checker-compat-qual/2.5.5/jar "org.checkerframework.checker.nullness.compatqual.NonNullDecl", "org.checkerframework.checker.nullness.compatqual.NonNullType", // https://janino-compiler.github.io/janino/apidocs/org/codehaus/commons/nullanalysis/NotNull.html "org.codehaus.commons.nullanalysis.NotNull", // https://help.eclipse.org/neon/index.jsp?topic=/org.eclipse.jdt.doc.isv/reference/api/org/eclipse/jdt/annotation/NonNull.html "org.eclipse.jdt.annotation.NonNull", - // https://github.com/eclipse/jgit/blob/master/org.eclipse.jgit/src/org/eclipse/jgit/annotations/NonNull.java + // https://git.eclipse.org/c/jdt/eclipse.jdt.core.git/tree/org.eclipse.jdt.annotation/src/org/eclipse/jdt/annotation/NonNull.java "org.eclipse.jgit.annotations.NonNull", - // https://github.com/JetBrains/intellij-community/blob/master/platform/annotations/java8/src/org/jetbrains/annotations/NotNull.java + // https://github.com/eclipse/lsp4j/blob/main/org.eclipse.lsp4j.jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/validation/NonNull.java + "org.eclipse.lsp4j.jsonrpc.validation.NonNull", + // https://www.jetbrains.com/help/idea/nullable-and-notnull-annotations.html "org.jetbrains.annotations.NotNull", // http://svn.code.sf.net/p/jmlspecs/code/JMLAnnotations/trunk/src/org/jmlspecs/annotation/NonNull.java "org.jmlspecs.annotation.NonNull", - // http://bits.netbeans.org/8.2/javadoc/org-netbeans-api-annotations-common/org/netbeans/api/annotations/common/NonNull.html + // http://bits.netbeans.org/dev/javadoc/org-netbeans-api-annotations-common/org/netbeans/api/annotations/common/NonNull.html "org.netbeans.api.annotations.common.NonNull", // https://github.com/spring-projects/spring-framework/blob/master/spring-core/src/main/java/org/springframework/lang/NonNull.java - "org.springframework.lang.NonNull"); + "org.springframework.lang.NonNull", + // https://github.com/reactor/reactor-core/blob/main/reactor-core/src/main/java/reactor/util/annotation/NonNull.java + "reactor.util.annotation.NonNull"); // List is in alphabetical order. If you update it, also update // ../../../../../../../../docs/manual/nullness-checker.tex . /** Aliases for {@code @Nullable}. */ private static final List<@FullyQualifiedName String> NULLABLE_ALIASES = Arrays.asList( - // https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/annotation/Nullable.java + // https://developer.android.com/reference/androidx/annotation/Nullable "android.annotation.Nullable", - // https://android.googlesource.com/platform/frameworks/support/+/master/annotations/src/main/java/android/support/annotation/Nullable.java + // https://developer.android.com/reference/android/support/annotation/Nullable "android.support.annotation.Nullable", - // https://android.googlesource.com/platform/frameworks/support/+/master/annotations/src/main/java/androidx/annotation/Nullable.java + // https://android.googlesource.com/platform/tools/metalava/+/fcb3d99ad3fe17a266d066b28ceb6c526f7aa415/stub-annotations/src/main/java/android/support/annotation/RecentlyNullable.java + "android.support.annotation.RecentlyNullable", + // https://developer.android.com/reference/androidx/annotation/Nullable "androidx.annotation.Nullable", // https://android.googlesource.com/platform/tools/metalava/+/master/stub-annotations/src/main/java/androidx/annotation/RecentlyNullable.java "androidx.annotation.RecentlyNullable", + // https://android.googlesource.com/platform/sdk/+/66fcecc/common/src/com/android/annotations/Nullable.java + "com.android.annotations.Nullable", + // https://github.com/lpantano/java_seqbuster/blob/master/AdRec/src/adrec/com/beust/jcommander/internal/Nullable.java + "com.beust.jcommander.internal.Nullable", + // https://github.com/cloudendpoints/endpoints-java/blob/master/endpoints-framework/src/main/java/com/google/api/server/spi/config/Nullable.java + "com.google.api.server.spi.config.Nullable", + // https://github.com/firebase/firebase-android-sdk/blob/master/firebase-database/src/main/java/com/google/firebase/database/annotations/Nullable.java + "com.google.firebase.database.annotations.Nullable", + // https://github.com/firebase/firebase-admin-java/blob/master/src/main/java/com/google/firebase/internal/Nullable.java + "com.google.firebase.internal.Nullable", + // https://gerrit.googlesource.com/gerrit/+/refs/heads/master/java/com/google/gerrit/common/Nullable.java + "com.google.gerrit.common.Nullable", + // https://github.com/mongodb/mongo-java-driver/blob/master/driver-core/src/main/com/mongodb/lang/Nullable.java + "com.mongodb.lang.Nullable", + // https://github.com/eclipse-ee4j/jaxb-istack-commons/blob/master/istack-commons/runtime/src/main/java/com/sun/istack/Nullable.java + "com.sun.istack.Nullable", + // https://github.com/openjdk/jdk8/blob/master/jaxws/src/share/jaxws_classes/com/sun/istack/internal/Nullable.java "com.sun.istack.internal.Nullable", + // https://github.com/pingidentity/ldapsdk/blob/master/src/com/unboundid/util/Nullable.java + "com.unboundid.util.Nullable", // http://findbugs.sourceforge.net/api/edu/umd/cs/findbugs/annotations/CheckForNull.html "edu.umd.cs.findbugs.annotations.CheckForNull", // http://findbugs.sourceforge.net/api/edu/umd/cs/findbugs/annotations/Nullable.html @@ -170,13 +220,26 @@ public class NullnessAnnotatedTypeFactory "edu.umd.cs.findbugs.annotations.PossiblyNull", // http://findbugs.sourceforge.net/api/edu/umd/cs/findbugs/annotations/UnknownNullness.html "edu.umd.cs.findbugs.annotations.UnknownNullness", + // https://github.com/micrometer-metrics/micrometer/blob/main/micrometer-core/src/main/java/io/micrometer/core/lang/Nullable.java + "io.micrometer.core.lang.Nullable", // https://github.com/ReactiveX/RxJava/blob/2.x/src/main/java/io/reactivex/annotations/Nullable.java "io.reactivex.annotations.Nullable", // https://github.com/ReactiveX/RxJava/blob/3.x/src/main/java/io/reactivex/rxjava3/annotations/Nullable.java "io.reactivex.rxjava3.annotations.Nullable", - // https://jcp.org/en/jsr/detail?id=305 + // https://jcp.org/en/jsr/detail?id=305; no documentation at + // https://www.javadoc.io/doc/com.google.code.findbugs/jsr305/3.0.1/javax/annotation/Nullable.html "javax.annotation.CheckForNull", "javax.annotation.Nullable", + // https://github.com/Pragmatists/JUnitParams/blob/master/src/main/java/junitparams/converters/Nullable.java + "junitparams.converters.Nullable", + // https://android.googlesource.com/platform/libcore/+/master/luni/src/main/java/libcore/util/Nullable.java + "libcore.util.Nullable", + // https://github.com/apache/avro/blob/master/lang/java/avro/src/main/java/org/apache/avro/reflect/Nullable.java + "org.apache.avro.reflect.Nullable", + // https://github.com/apache/cxf/blob/master/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/ext/Nullable.java + "org.apache.cxf.jaxrs.ext.Nullable", + // https://github.com/gatein/gatein-shindig/blob/master/java/common/src/main/java/org/apache/shindig/common/Nullable.java + "org.apache.shindig.common.Nullable", // https://search.maven.org/search?q=a:checker-compat-qual "org.checkerframework.checker.nullness.compatqual.NullableDecl", "org.checkerframework.checker.nullness.compatqual.NullableType", @@ -184,23 +247,27 @@ public class NullnessAnnotatedTypeFactory "org.codehaus.commons.nullanalysis.Nullable", // https://help.eclipse.org/neon/index.jsp?topic=/org.eclipse.jdt.doc.isv/reference/api/org/eclipse/jdt/annotation/Nullable.html "org.eclipse.jdt.annotation.Nullable", - // https://github.com/eclipse/jgit/blob/master/org.eclipse.jgit/src/org/eclipse/jgit/annotations/Nullable.java + // https://git.eclipse.org/c/jdt/eclipse.jdt.core.git/tree/org.eclipse.jdt.annotation/src/org/eclipse/jdt/annotation/Nullable.java "org.eclipse.jgit.annotations.Nullable", - // https://github.com/JetBrains/intellij-community/blob/master/platform/annotations/java8/src/org/jetbrains/annotations/Nullable.java + // https://www.jetbrains.com/help/idea/nullable-and-notnull-annotations.html "org.jetbrains.annotations.Nullable", + // https://github.com/JetBrains/java-annotations/blob/master/java8/src/main/java/org/jetbrains/annotations/UnknownNullability.java + "org.jetbrains.annotations.UnknownNullability", // http://svn.code.sf.net/p/jmlspecs/code/JMLAnnotations/trunk/src/org/jmlspecs/annotation/Nullable.java "org.jmlspecs.annotation.Nullable", // https://github.com/jspecify/jspecify/tree/main/src/main/java/org/jspecify/nullness "org.jspecify.nullness.Nullable", "org.jspecify.nullness.NullnessUnspecified", - // http://bits.netbeans.org/8.2/javadoc/org-netbeans-api-annotations-common/org/netbeans/api/annotations/common/CheckForNull.html + // http://bits.netbeans.org/dev/javadoc/org-netbeans-api-annotations-common/org/netbeans/api/annotations/common/CheckForNull.html "org.netbeans.api.annotations.common.CheckForNull", - // http://bits.netbeans.org/8.2/javadoc/org-netbeans-api-annotations-common/org/netbeans/api/annotations/common/NullAllowed.html + // http://bits.netbeans.org/dev/javadoc/org-netbeans-api-annotations-common/org/netbeans/api/annotations/common/NullAllowed.html "org.netbeans.api.annotations.common.NullAllowed", - // http://bits.netbeans.org/8.2/javadoc/org-netbeans-api-annotations-common/org/netbeans/api/annotations/common/NullUnknown.html + // http://bits.netbeans.org/dev/javadoc/org-netbeans-api-annotations-common/org/netbeans/api/annotations/common/NullUnknown.html "org.netbeans.api.annotations.common.NullUnknown", // https://github.com/spring-projects/spring-framework/blob/master/spring-core/src/main/java/org/springframework/lang/Nullable.java - "org.springframework.lang.Nullable"); + "org.springframework.lang.Nullable", + // https://github.com/reactor/reactor-core/blob/main/reactor-core/src/main/java/reactor/util/annotation/Nullable.java + "reactor.util.annotation.Nullable"); /** * Creates a NullnessAnnotatedTypeFactory. @@ -311,9 +378,8 @@ public Pair, List> getUninitializedFields( } @Override - protected NullnessAnalysis createFlowAnalysis( - List> fieldValues) { - return new NullnessAnalysis(checker, this, fieldValues); + protected NullnessAnalysis createFlowAnalysis() { + return new NullnessAnalysis(checker, this); } @Override @@ -358,16 +424,18 @@ public ParameterizedExecutableType methodFromUse(MethodInvocationTree tree) { @Override public void adaptGetClassReturnTypeToReceiver( - final AnnotatedExecutableType getClassType, final AnnotatedTypeMirror receiverType) { + final AnnotatedExecutableType getClassType, + final AnnotatedTypeMirror receiverType, + ExpressionTree tree) { - super.adaptGetClassReturnTypeToReceiver(getClassType, receiverType); + super.adaptGetClassReturnTypeToReceiver(getClassType, receiverType, tree); - // Make the wildcard always @NonNull, regardless of the declared type. + // Make the captured wildcard always @NonNull, regardless of the declared type. final AnnotatedDeclaredType returnAdt = (AnnotatedDeclaredType) getClassType.getReturnType(); final List typeArgs = returnAdt.getTypeArguments(); - final AnnotatedWildcardType classWildcardArg = (AnnotatedWildcardType) typeArgs.get(0); - classWildcardArg.getExtendsBoundField().replaceAnnotation(NONNULL); + AnnotatedTypeVariable classWildcardArg = (AnnotatedTypeVariable) typeArgs.get(0); + classWildcardArg.getUpperBound().replaceAnnotation(NONNULL); } @Override @@ -636,7 +704,8 @@ protected boolean isSubtypeWithElements( if (!subKind.isInSameHierarchyAs(NULLABLE) || !superKind.isInSameHierarchyAs(NULLABLE)) { return this.isSubtypeInitialization(subAnno, subKind, superAnno, superKind); } - throw new BugInCF("Unexpected annotations isSubtypeWithElements(%s, %s)", subAnno, superAnno); + throw new TypeSystemError( + "Unexpected annotations isSubtypeWithElements(%s, %s)", subAnno, superAnno); } @Override @@ -650,7 +719,8 @@ protected AnnotationMirror leastUpperBoundWithElements( || !qualifierKind2.isInSameHierarchyAs(NULLABLE)) { return this.leastUpperBoundInitialization(a1, qualifierKind1, a2, qualifierKind2); } - throw new BugInCF("Unexpected annotations leastUpperBoundWithElements(%s, %s)", a1, a2); + throw new TypeSystemError( + "Unexpected annotations leastUpperBoundWithElements(%s, %s)", a1, a2); } @Override @@ -664,7 +734,8 @@ protected AnnotationMirror greatestLowerBoundWithElements( || !qualifierKind2.isInSameHierarchyAs(NULLABLE)) { return this.greatestLowerBoundInitialization(a1, qualifierKind1, a2, qualifierKind2); } - throw new BugInCF("Unexpected annotations greatestLowerBoundWithElements(%s, %s)", a1, a2); + throw new TypeSystemError( + "Unexpected annotations greatestLowerBoundWithElements(%s, %s)", a1, a2); } } @@ -799,72 +870,61 @@ public void wpiAdjustForUpdateNonField(AnnotatedTypeMirror rhsATM) { // * check for @MonotonicNonNull // * output @RequiresNonNull rather than @RequiresQualifier. @Override - public List getPreconditionAnnotation( - VariableElement elt, AnnotatedTypeMirror fieldType) { - AnnotatedTypeMirror declaredType = fromElement(elt); + protected @Nullable AnnotationMirror createRequiresOrEnsuresQualifier( + String expression, + AnnotationMirror qualifier, + AnnotatedTypeMirror declaredType, + Analysis.BeforeOrAfter preOrPost, + @Nullable List preconds) { // TODO: This does not handle the possibility that the user set a different default annotation. if (!(declaredType.hasAnnotation(NULLABLE) || declaredType.hasAnnotation(POLYNULL) || declaredType.hasAnnotation(MONOTONIC_NONNULL))) { - return Collections.emptyList(); + return null; + } + + if (preOrPost == BeforeOrAfter.AFTER + && declaredType.hasAnnotation(MONOTONIC_NONNULL) + && preconds.contains(requiresNonNullAnno(expression))) { + // The postcondition is implied by the precondition and the field being @MonotonicNonNull. + return null; } - if (AnnotationUtils.containsSameByName( - fieldType.getAnnotations(), "org.checkerframework.checker.nullness.qual.NonNull")) { - return requiresNonNullAnno(elt); + if (AnnotationUtils.areSameByName( + qualifier, "org.checkerframework.checker.nullness.qual.NonNull")) { + if (preOrPost == BeforeOrAfter.BEFORE) { + return requiresNonNullAnno(expression); + } else { + return ensuresNonNullAnno(expression); + } } - return Collections.emptyList(); + return super.createRequiresOrEnsuresQualifier( + expression, qualifier, declaredType, preOrPost, preconds); } /** - * Returns a {@code RequiresNonNull("...")} annotation for the given field. + * Returns a {@code RequiresNonNull("...")} annotation for the given expression. * - * @param fieldElement a field - * @return a {@code RequiresNonNull("...")} annotation for the given field + * @param expression an expression + * @return a {@code RequiresNonNull("...")} annotation for the given expression */ - private List requiresNonNullAnno(VariableElement fieldElement) { + private AnnotationMirror requiresNonNullAnno(String expression) { AnnotationBuilder builder = new AnnotationBuilder(processingEnv, RequiresNonNull.class); - String receiver = JavaExpression.getImplicitReceiver(fieldElement).toString(); - String expression = receiver + "." + fieldElement.getSimpleName(); builder.setValue("value", new String[] {expression}); AnnotationMirror am = builder.build(); - return Collections.singletonList(am); - } - - @Override - public List getPostconditionAnnotation( - VariableElement elt, AnnotatedTypeMirror fieldAnnos, List preconds) { - AnnotatedTypeMirror declaredType = fromElement(elt); - // TODO: This does not handle the possibility that the user set a different default annotation. - if (!(declaredType.hasAnnotation(NULLABLE) - || declaredType.hasAnnotation(POLYNULL) - || declaredType.hasAnnotation(MONOTONIC_NONNULL))) { - return Collections.emptyList(); - } - if (declaredType.hasAnnotation(MONOTONIC_NONNULL) - && preconds.contains(requiresNonNullAnno(elt))) { - // The postcondition is implied by the precondition and the field being @MonotonicNonNull. - return Collections.emptyList(); - } - if (AnnotationUtils.containsSameByName( - fieldAnnos.getAnnotations(), "org.checkerframework.checker.nullness.qual.NonNull")) { - return ensuresNonNullAnno(elt); - } - return Collections.emptyList(); + return am; } /** - * Returns a {@code EnsuresNonNull("...")} annotation for the given field. + * Returns a {@code EnsuresNonNull("...")} annotation for the given expression. * - * @param fieldElement a field - * @return a {@code EnsuresNonNull("...")} annotation for the given field + * @param expression an expression + * @return a {@code EnsuresNonNull("...")} annotation for the given expression */ - private List ensuresNonNullAnno(VariableElement fieldElement) { + private AnnotationMirror ensuresNonNullAnno(String expression) { AnnotationBuilder builder = new AnnotationBuilder(processingEnv, EnsuresNonNull.class); - String receiver = JavaExpression.getImplicitReceiver(fieldElement).toString(); - String expression = receiver + "." + fieldElement.getSimpleName(); builder.setValue("value", new String[] {expression}); AnnotationMirror am = builder.build(); - return Collections.singletonList(am); + return am; } } diff --git a/checker/src/main/java/org/checkerframework/checker/nullness/NullnessTransfer.java b/checker/src/main/java/org/checkerframework/checker/nullness/NullnessTransfer.java index 8a505b85a1..258093d720 100644 --- a/checker/src/main/java/org/checkerframework/checker/nullness/NullnessTransfer.java +++ b/checker/src/main/java/org/checkerframework/checker/nullness/NullnessTransfer.java @@ -41,8 +41,8 @@ import org.checkerframework.framework.util.AnnotatedTypes; import org.checkerframework.javacutil.AnnotationBuilder; import org.checkerframework.javacutil.AnnotationUtils; -import org.checkerframework.javacutil.BugInCF; import org.checkerframework.javacutil.TreeUtils; +import org.checkerframework.javacutil.TypeSystemError; import org.checkerframework.javacutil.TypesUtils; /** @@ -401,7 +401,7 @@ private boolean hasNullableValueType(AnnotatedTypeMirror mapOrSubtype) { AnnotatedTypes.asSuper(nullnessTypeFactory, mapOrSubtype, MAP_TYPE); int numTypeArguments = mapType.getTypeArguments().size(); if (numTypeArguments != 2) { - throw new BugInCF("Wrong number %d of type arguments: %s", numTypeArguments, mapType); + throw new TypeSystemError("Wrong number %d of type arguments: %s", numTypeArguments, mapType); } AnnotatedTypeMirror valueType = mapType.getTypeArguments().get(1); return valueType.hasAnnotation(NULLABLE); diff --git a/checker/src/main/java/org/checkerframework/checker/regex/RegexAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/regex/RegexAnnotatedTypeFactory.java index c11ff8a00b..4156498c41 100644 --- a/checker/src/main/java/org/checkerframework/checker/regex/RegexAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/regex/RegexAnnotatedTypeFactory.java @@ -42,8 +42,8 @@ import org.checkerframework.framework.util.QualifierKind; import org.checkerframework.javacutil.AnnotationBuilder; import org.checkerframework.javacutil.AnnotationUtils; -import org.checkerframework.javacutil.BugInCF; import org.checkerframework.javacutil.TreeUtils; +import org.checkerframework.javacutil.TypeSystemError; /** * Adds {@link Regex} to the type of tree, in the following cases: @@ -188,7 +188,7 @@ protected boolean isSubtypeWithElements( } else if (subKind == PARTIALREGEX_KIND && superKind == PARTIALREGEX_KIND) { return AnnotationUtils.areSame(subAnno, superAnno); } - throw new BugInCF("Unexpected qualifiers: %s %s", subAnno, superAnno); + throw new TypeSystemError("Unexpected qualifiers: %s %s", subAnno, superAnno); } @Override @@ -217,7 +217,7 @@ protected AnnotationMirror leastUpperBoundWithElements( } else if (qualifierKind2 == PARTIALREGEX_KIND || qualifierKind2 == REGEX_KIND) { return a2; } - throw new BugInCF("Unexpected qualifiers: %s %s", a1, a2); + throw new TypeSystemError("Unexpected qualifiers: %s %s", a1, a2); } @Override @@ -246,7 +246,7 @@ protected AnnotationMirror greatestLowerBoundWithElements( } else if (qualifierKind2 == PARTIALREGEX_KIND || qualifierKind2 == REGEX_KIND) { return a2; } - throw new BugInCF("Unexpected qualifiers: %s %s", a1, a2); + throw new TypeSystemError("Unexpected qualifiers: %s %s", a1, a2); } /** diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java index 3d380bf31b..9f580e8885 100644 --- a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java +++ b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java @@ -4,7 +4,6 @@ import com.google.common.collect.FluentIterable; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; -import com.sun.source.tree.ConditionalExpressionTree; import com.sun.source.tree.MethodInvocationTree; import com.sun.source.tree.MethodTree; import com.sun.source.tree.Tree; @@ -28,6 +27,7 @@ import javax.lang.model.element.ElementKind; import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.Name; +import javax.lang.model.element.TypeElement; import javax.lang.model.element.VariableElement; import javax.lang.model.type.TypeKind; import javax.lang.model.type.TypeMirror; @@ -36,6 +36,7 @@ import org.checkerframework.checker.mustcall.MustCallAnnotatedTypeFactory; import org.checkerframework.checker.mustcall.MustCallChecker; import org.checkerframework.checker.mustcall.qual.MustCall; +import org.checkerframework.checker.mustcall.qual.MustCallAlias; import org.checkerframework.checker.mustcall.qual.NotOwning; import org.checkerframework.checker.mustcall.qual.Owning; import org.checkerframework.checker.nullness.qual.Nullable; @@ -55,7 +56,6 @@ import org.checkerframework.dataflow.cfg.node.NullLiteralNode; import org.checkerframework.dataflow.cfg.node.ObjectCreationNode; import org.checkerframework.dataflow.cfg.node.ReturnNode; -import org.checkerframework.dataflow.cfg.node.TernaryExpressionNode; import org.checkerframework.dataflow.cfg.node.ThisNode; import org.checkerframework.dataflow.cfg.node.TypeCastNode; import org.checkerframework.dataflow.expression.FieldAccess; @@ -66,54 +66,67 @@ import org.checkerframework.framework.flow.CFAnalysis; import org.checkerframework.framework.flow.CFStore; import org.checkerframework.framework.flow.CFValue; +import org.checkerframework.framework.type.AnnotatedTypeMirror; import org.checkerframework.framework.util.JavaExpressionParseUtil.JavaExpressionParseException; import org.checkerframework.framework.util.StringToJavaExpression; import org.checkerframework.javacutil.AnnotationUtils; -import org.checkerframework.javacutil.BugInCF; import org.checkerframework.javacutil.ElementUtils; import org.checkerframework.javacutil.Pair; import org.checkerframework.javacutil.TreePathUtil; import org.checkerframework.javacutil.TreeUtils; +import org.checkerframework.javacutil.TypeSystemError; import org.checkerframework.javacutil.TypesUtils; import org.plumelib.util.StringsPlume; /** - * An analyzer that checks consistency of {@code @MustCall} and {@code @CalledMethods} types, - * thereby detecting resource leaks. For any expression e the analyzer ensures that when - * e goes out of scope, there exists a resource alias r of e (which might - * be e itself) such that the must-call methods of r (i.e. the values of - * r's MustCall type) are contained in the value of r's CalledMethods type. For - * any e for which this property does not hold, the analyzer reports a {@code + * An analyzer that checks consistency of {@link MustCall} and {@link CalledMethods} types, thereby + * detecting resource leaks. For any expression e the analyzer ensures that when e + * goes out of scope, there exists a resource alias r of e (which might be + * e itself) such that the must-call methods of r (i.e. the values of r's + * MustCall type) are contained in the value of r's CalledMethods type. For any e + * for which this property does not hold, the analyzer reports a {@code * "required.method.not.called"} error, indicating a possible resource leak. * - *

Mechanically, the analysis tracks dataflow facts about the obligations of sets of - * resource-aliases that refer to the same resource, and checks their must-call and called-methods - * types when the last reference to those sets goes out of scope. That is, this class implements a - * lightweight alias analysis that tracks must-alias sets for resources. + *

Mechanically, the analysis does two tasks. * - *

Throughout this class, variables named "obligation" or "obligations" are dataflow facts of - * type {@link Obligation}, each representing a set of resource aliases for some value with a - * non-empty {@code @MustCall} obligation. These obligations can be resolved either via ownership - * transfer (e.g. by being assigned into an owning field) or via their must-call obligations being - * contained in their called-methods type when the last reference in a set goes out of scope. + *

    + *
  • Tracks must-aliases, implemented via a dataflow analysis. Each dataflow fact is a set of + * resource-aliases that refer to the same resource. Furthermore, that resource is owned. No + * dataflow facts are maintained for a non-owned resource. + *
  • When the last resource alias in a resource-alias set goes out-of-scope, it checks their + * must-call and called-methods types. The analysis does not track must-call or called-methods + * types, but queries other checkers to obtain them. + *
+ * + *

Class {@link Obligation} represents a single such dataflow fact. Abstractly, each dataflow + * fact is a pair: a set of resource aliases to some resource, and the must-call obligations of that + * resource (i.e the list of must-call methods that need to be called on one of the resource + * aliases). Concretely, the Must Call Checker is responsible for tracking the latter - an + * expression's must-call type indicates which methods must be called - so this dataflow analysis + * only actually tracks the sets of resource aliases. * - *

The algorithm here adds, modifies, or removes obligations from those it is tracking when - * certain code patterns are encountered. Here are non-exhaustive examples: + *

The dataflow algorithm adds, modifies, or removes dataflow facts when certain code patterns + * are encountered, to account for ownership transfer. Here are non-exhaustive examples: * *

    - *
  • A new obligation is added to the tracked set when a constructor or a method with an owning - * return is invoked. - *
  • An obligation is modified when an expression with a tracked obligation is assigned to a - * local variable or a resource-alias method or constructor is called (the new local or the - * result of the resource-alias method/constructor is added to the existing resource alias - * set). - *
  • An obligation can be removed when a member of a resource-alias set is assigned to an owning - * field or passed to a method in a parameter location that is annotated as {@code @Owning}. + *
  • A new fact is added to the tracked set when a constructor or a method with an owning return + * is invoked. + *
  • A fact is modified when an expression with a tracked Obligation is the RHS of a + * (pseudo-)assignment. The LHS is added to the existing resource alias set. + *
  • A fact can be removed when a member of a resource-alias set is assigned to an owning field + * or passed to a method in a parameter location that is annotated as {@code @Owning}. *
* + *

The dataflow analysis for these Obligations is conservative in that it guarantees that for + * every resource which actually does have a must-call obligation, at least one Obligation will + * exist. However, it does not guarantee the opposite: Obligations may also exist for resources + * without a must-call obligation (or for non-resources) as a result of analysis imprecision. That + * is, the set of Obligations tracked by the analysis over-approximates the actual set of resources + * in the analyzed program with must-call obligations. + * *

Throughout, this class uses the temporary-variable facilities provided by the Must Call and - * Resource Leak type factories to both emulate a three-address-form IR, simplifying some analysis - * logic, and to permit expressions to have their types refined in their respective checkers' + * Resource Leak type factories both to emulate a three-address-form IR (simplifying some analysis + * logic) and to permit expressions to have their types refined in their respective checkers' * stores. These temporary variables can be members of resource-alias sets. Without temporary * variables, the checker wouldn't be able to verify code such as {@code new Socket(host, * port).close()}, which would cause false positives. Temporaries are created for {@code new} @@ -124,8 +137,8 @@ class MustCallConsistencyAnalyzer { /** - * Aliases through which the checker has already reported about a resource leak, to avoid - * duplicate reports. + * Aliases about which the checker has already reported about a resource leak, to avoid duplicate + * reports. */ private final Set reportedErrorAliases = new HashSet<>(); @@ -141,6 +154,286 @@ class MustCallConsistencyAnalyzer { /** The analysis from the Resource Leak Checker, used to get input stores based on CFG blocks. */ private final CFAnalysis analysis; + /** + * An Obligation is a dataflow fact: a set of resource aliases. Abstractly, each Obligation + * represents a resource that the analyzed program which might have a must-call obligation. Each + * Obligation is a pair of a set of resource aliases and their must-call obligation. Must-call + * obligations are tracked by the {@link MustCallChecker} and are accessed by looking up the + * type(s) in its type system of the resource aliases contained in each {@code Obligation} using + * {@link #getMustCallMethods(ResourceLeakAnnotatedTypeFactory, CFStore)}. + * + *

There is no guarantee that a given Obligation represents a resource with a real must-call + * obligation. When the analysis can conclude that a given Obligation certainly does not represent + * a real resource with a real must-call obligation (such as if the only resource alias is + * certainly a null pointer, or if the must-call obligation is the empty set), the analysis can + * discard the Obligation. + */ + /* package-private */ static class Obligation { + + /** + * The set of resource aliases through which a must-call obligation can be satisfied. Calling + * the required method(s) in the must-call obligation through any of them satisfies the + * must-call obligation: that is, if the called-methods type of any alias contains the required + * method(s), then the must-call obligation is satisfied. See {@link #getMustCallMethods}. + * + *

{@code Obligation} is deeply immutable. If some code were to accidentally mutate a {@code + * resourceAliases} set it could be really nasty to debug, so this set is always immutable. + */ + public final ImmutableSet resourceAliases; + + /** + * Create an Obligation from a set of resource aliases. + * + * @param resourceAliases a set of resource aliases + */ + public Obligation(Set resourceAliases) { + this.resourceAliases = ImmutableSet.copyOf(resourceAliases); + } + + /** + * Returns the resource alias in this Obligation's resource alias set corresponding to {@code + * localVariableNode} if one is present. Otherwise, returns null. + * + * @param localVariableNode a local variable + * @return the resource alias corresponding to {@code localVariableNode} if one is present; + * otherwise, null + */ + private @Nullable ResourceAlias getResourceAlias(LocalVariableNode localVariableNode) { + Element element = localVariableNode.getElement(); + for (ResourceAlias alias : resourceAliases) { + if (alias.reference.getElement().equals(element)) { + return alias; + } + } + return null; + } + + /** + * Returns the resource alias in this Obligation's resource alias set corresponding to {@code + * expression} if one is present. Otherwise, returns null. + * + * @param expression a Java expression + * @return the resource alias corresponding to {@code expression} if one is present; otherwise, + * null + */ + private @Nullable ResourceAlias getResourceAlias(JavaExpression expression) { + for (ResourceAlias alias : resourceAliases) { + if (alias.reference.equals(expression)) { + return alias; + } + } + return null; + } + + /** + * Returns true if this contains a resource alias corresponding to {@code localVariableNode}, + * meaning that calling the required methods on {@code localVariableNode} is sufficient to + * satisfy the must-call obligation this object represents. + * + * @param localVariableNode a local variable node + * @return true if a resource alias corresponding to {@code localVariableNode} is present + */ + private boolean canBeSatisfiedThrough(LocalVariableNode localVariableNode) { + return getResourceAlias(localVariableNode) != null; + } + + /** + * Does this Obligation contain any resource aliases that were derived from {@link + * MustCallAlias} parameters? + * + * @return the logical or of the {@link ResourceAlias#derivedFromMustCallAliasParam} fields of + * this Obligation's resource aliases + */ + public boolean derivedFromMustCallAlias() { + for (ResourceAlias ra : resourceAliases) { + if (ra.derivedFromMustCallAliasParam) { + return true; + } + } + return false; + } + + /** + * Gets the must-call methods (i.e. the list of methods that must be called to satisfy the + * must-call obligation) of the resource represented by this Obligation. + * + * @param rlAtf a Resource Leak Annotated Type Factory + * @param mcStore a CFStore produced by the MustCall checker's dataflow analysis. If this is + * null, then the default MustCall type of each variable's class will be used. + * @return the list of must-call method names, or null if the resource's must-call obligations + * are unsatisfiable (i.e. its value in the Must Call store is MustCallUnknown) + */ + public @Nullable List getMustCallMethods( + ResourceLeakAnnotatedTypeFactory rlAtf, @Nullable CFStore mcStore) { + MustCallAnnotatedTypeFactory mustCallAnnotatedTypeFactory = + rlAtf.getTypeFactoryOfSubchecker(MustCallChecker.class); + + // Need to get the LUB (ie, union) of the MC values, because if a CreatesMustCallFor method + // was called on just one of the aliases then they all need to be treated as if they need to + // call the relevant methods. + AnnotationMirror mcLub = mustCallAnnotatedTypeFactory.BOTTOM; + for (ResourceAlias alias : this.resourceAliases) { + AnnotationMirror mcAnno = getMustCallValue(alias, mcStore, mustCallAnnotatedTypeFactory); + mcLub = mustCallAnnotatedTypeFactory.getQualifierHierarchy().leastUpperBound(mcLub, mcAnno); + } + if (AnnotationUtils.areSameByName( + mcLub, "org.checkerframework.checker.mustcall.qual.MustCall")) { + return rlAtf.getMustCallValues(mcLub); + } else { + return null; + } + } + + /** + * Gets the must-call type associated with the given resource alias, falling on back on the + * declared type if there is no refined type for the alias in the store. + * + * @param alias a resource alias + * @param mcStore the must-call checker's store + * @param mcAtf the must-call checker's annotated type factory + * @return the annotation from the must-call type hierarchy associated with {@code alias} + */ + private static AnnotationMirror getMustCallValue( + ResourceAlias alias, @Nullable CFStore mcStore, MustCallAnnotatedTypeFactory mcAtf) { + LocalVariable reference = alias.reference; + CFValue value = mcStore == null ? null : mcStore.getValue(reference); + if (value != null) { + AnnotationMirror result = + AnnotationUtils.getAnnotationByClass(value.getAnnotations(), MustCall.class); + if (result != null) { + return result; + } + } + // There wasn't an @MustCall annotation for it in the store, so fall back to the default + // must-call type for the class. + // TODO: we currently end up in this case when checking a call to the return type + // of a returns-receiver method on something with a MustCall type; for example, + // see tests/socket/ZookeeperReport6.java. We should instead use a poly type if we can. + TypeElement typeElt = TypesUtils.getTypeElement(reference.getType()); + if (typeElt == null) { + // typeElt is null if reference.getType() was not a class, interface, annotation type, + // or enum -- that is, was not an annotatable type. + // That happens rarely, such as when it is a wildcard type. In these cases, fall back + // on a safe default: top. + return mcAtf.TOP; + } + if (typeElt.asType().getKind() == TypeKind.VOID) { + // Void types can't have methods called on them, so returning bottom is safe. + return mcAtf.BOTTOM; + } + return mcAtf.getAnnotatedType(typeElt).getAnnotationInHierarchy(mcAtf.TOP); + } + + @Override + public String toString() { + return "Obligation: resourceAliases=" + Iterables.toString(resourceAliases); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + Obligation that = (Obligation) obj; + return this.resourceAliases.equals(that.resourceAliases); + } + + @Override + public int hashCode() { + return Objects.hash(resourceAliases); + } + } + + // Is there a different Obligation on every line of the program, or is Obligation mutable? + // (Or maybe Obligation is abstractly mutable when you consider the @MustCall types that are not + // recorded in Obligation's representation.) Could you clarify? I found the first paragraph + // confusing, including "correspond to". + /** + * A resource alias is a reference through which a must-call obligation can be satisfied. Any + * must-call obligation might be satisfiable through one or more resource aliases. An {@link + * Obligation} tracks one set of resource aliases that correspond to one must-call obligation in + * the program. + * + *

A resource alias is always owning; non-owning aliases are, by definition, not tracked. + * + *

Internally, a resource alias is represented by a pair of a local or temporary variable (the + * "reference" through which the must-call obligations for the alias set to which it belongs can + * be satisfied) and a tree that "assigns" the reference. + */ + /* package-private */ static class ResourceAlias { + + /** A local variable defined in the source code or a temporary variable for an expression. */ + public final LocalVariable reference; + + /** The tree at which {@code reference} was assigned, for the purpose of error reporting */ + public final Tree tree; + + /** + * Was this ResourceAlias derived from a parameter to a method that was annotated as {@link + * MustCallAlias}? If so, the obligation containing this resource alias must be discharged only + * in one of the following ways: + * + *

    + *
  • it is passed to another method or constructor in an @MustCallAlias position, and then + * the containing method returns that method’s result, or the call is a super() + * constructor call annotated with {@link MustCallAlias}, or + *
  • it is stored in an owning field of the class under analysis + *
+ */ + public final boolean derivedFromMustCallAliasParam; + + /** + * Create a new resource alias. This constructor should only be used if the resource alias was + * not derived from a method parameter annotated as {@link MustCallAlias}. + * + * @param reference the local variable + * @param tree the tree + */ + public ResourceAlias(LocalVariable reference, Tree tree) { + this(reference, tree, false); + } + + /** + * Create a new resource alias. + * + * @param reference the local variable + * @param tree the tree + * @param derivedFromMustCallAliasParam true iff this resource alias was created because of an + * {@link MustCallAlias} parameter + */ + public ResourceAlias( + LocalVariable reference, Tree tree, boolean derivedFromMustCallAliasParam) { + this.reference = reference; + this.tree = tree; + this.derivedFromMustCallAliasParam = derivedFromMustCallAliasParam; + } + + @Override + public String toString() { + return "(ResourceAlias: reference: " + reference + " |||| tree: " + tree + ")"; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ResourceAlias that = (ResourceAlias) o; + return reference.equals(that.reference) && tree.equals(that.tree); + } + + @Override + public int hashCode() { + return Objects.hash(reference, tree); + } + } + /** * Creates a consistency analyzer. Typically, the type factory's postAnalyze method would * instantiate a new consistency analyzer using this constructor and then call {@link @@ -159,9 +452,17 @@ class MustCallConsistencyAnalyzer { /** * The main function of the consistency dataflow analysis. The analysis tracks dataflow facts - * ("obligations") of type {@link Obligation}, each representing a set of resource aliases for - * some value with a non-empty {@code @MustCall} obligation. (It is not necessary to track - * expressions with empty {@code @MustCall} obligations, because they are trivially fulfilled.) + * ("Obligations") of type {@link Obligation}, each representing a set of owning resource aliases + * for some value with a non-empty {@code @MustCall} obligation. The set of tracked Obligations is + * guaranteed to include at least one Obligation for each actual resource in the program, but + * might include other, spurious Obligations, too (that is, it is a conservative + * over-approximation of the true Obligation set). + * + *

The analysis improves its precision by removing Obligations from tracking when it can prove + * that they do not represent real resources. For example, it is not necessary to track + * expressions with empty {@code @MustCall} obligations, because they are trivially fulfilled. Nor + * is tracking non-owning aliases necessary, because by definition they cannot be used to fulfill + * must-call obligations. * * @param cfg the control flow graph of the method to check */ @@ -171,7 +472,7 @@ class MustCallConsistencyAnalyzer { void analyze(ControlFlowGraph cfg) { // The `visited` set contains everything that has been added to the worklist, even if it has not // yet been removed and analyzed. - Set visited = new LinkedHashSet<>(); + Set visited = new HashSet<>(); Deque worklist = new ArrayDeque<>(); // Add any owning parameters to the initial set of variables to track. @@ -182,36 +483,35 @@ void analyze(ControlFlowGraph cfg) { while (!worklist.isEmpty()) { BlockWithObligations current = worklist.remove(); - List nodes = current.block.getNodes(); - // A *mutable* set that eventually holds the set of obligations to be propagated to successor - // blocks. The set is initialized to the current obligations and updated by the methods - // invoked in the for loop below. + // A *mutable* set that eventually holds the set of dataflow facts to be propagated to + // successor blocks. The set is initialized to the current dataflow facts and updated by the + // methods invoked in the for loop below. Set obligations = new LinkedHashSet<>(current.obligations); - for (Node node : nodes) { + for (Node node : current.block.getNodes()) { if (node instanceof AssignmentNode) { - handleAssignment((AssignmentNode) node, obligations); + updateObligationsForAssignment(obligations, (AssignmentNode) node); } else if (node instanceof ReturnNode) { - handleReturn((ReturnNode) node, cfg, obligations); + updateObligationsForOwningReturn(obligations, cfg, (ReturnNode) node); } else if (node instanceof MethodInvocationNode || node instanceof ObjectCreationNode) { - handleInvocation(obligations, node); + updateObligationsForInvocation(obligations, node); } // All other types of nodes are ignored. This is safe, because other kinds of // nodes cannot create or modify the resource-alias sets that the algorithm is tracking. } - handleSuccessorBlocks(visited, worklist, obligations, current.block); + propagateObligationsToSuccessorBlocks(obligations, current.block, visited, worklist); } } /** - * Update a set of obligations to account for a method or constructor invocation. + * Update a set of Obligations to account for a method or constructor invocation. * - * @param obligations the obligations to update + * @param obligations the Obligations to update * @param node the method or constructor invocation */ - private void handleInvocation(Set obligations, Node node) { - transferOwnershipToParameters(obligations, node); + private void updateObligationsForInvocation(Set obligations, Node node) { + removeObligationsAtOwnershipTransferToParameters(obligations, node); if (node instanceof MethodInvocationNode && typeFactory.canCreateObligations() && typeFactory.hasCreatesMustCallFor((MethodInvocationNode) node)) { @@ -226,89 +526,81 @@ private void handleInvocation(Set obligations, Node node) { return; } - if (typeFactory.hasDeclaredMustCall(node.getTree())) { + if (typeFactory.declaredTypeHasMustCall(node.getTree())) { // The incrementNumMustCall call above increments the count for the target of the // @CreatesMustCallFor annotation. By contrast, this call increments the count for the return // value of the method (which can't be the target of the annotation, because our syntax // doesn't support that). incrementNumMustCall(node); } - trackInvocationResult(obligations, node); - } - - /** - * If node is an invocation of a this or super constructor that has a MustCallAlias return type - * and a MustCallAlias parameter, check if any variable in the current set of obligations is being - * passed to the other constructor. If so, remove it from the obligations. - * - * @param obligations current obligations - * @param node a super or this constructor invocation - */ - private void handleThisOrSuperConstructorMustCallAlias(Set obligations, Node node) { - Node mustCallAliasArgument = getMustCallAliasArgumentNode(node); - // If the MustCallAlias argument is also in the set of obligations, then remove it -- its - // obligation has been fulfilled by being passed on to the MustCallAlias constructor (because a - // this/super constructor call can only occur in the body of another constructor). - if (mustCallAliasArgument instanceof LocalVariableNode) { - removeObligationsContainingVar(obligations, (LocalVariableNode) mustCallAliasArgument); - } + updateObligationsWithInvocationResult(obligations, node); } /** * Checks that an invocation of a CreatesMustCallFor method is valid. * - *

Such an invocation is valid for all expressions in the CreatesMustCallFor annotation one of - * of the following conditions are true: 1) the expression is an owning pointer, 2) the expression - * already has a tracked obligation, or 3) the method in which the invocation occurs also has - * an @CreatesMustCallFor annotation, with the same expression. + *

Such an invocation is valid if any of the conditions in {@link + * #isValidCreatesMustCallForExpression(Set, JavaExpression, TreePath)} is true. If none of these + * conditions are true, this method issues a reset.not.owning error. * - *

If none of the above are true, this method issues a reset.not.owning error. + *

For soundness, this method also guarantees that if any of the expressions in the + * CreatesMustCallFor annotation has a tracked Obligation, any tracked resource aliases of it will + * be removed (lest the analysis conclude that it is already closed because one of these aliases + * was closed before the method was invoked). Aliases created after the CreatesMustCallFor method + * is invoked are still permitted. * - *

For soundness, this method also guarantees that if the expression has a tracked obligation, - * any tracked aliases will be removed (lest the analysis conclude that it is already closed - * because one of these aliases was closed before the method was invoked). Aliases created after - * the CreatesMustCallFor method is invoked are still permitted. - * - * @param obligations the currently-tracked obligations; this value is side-effected if it - * contains an expression that is whose must-call obligation is reset + * @param obligations the currently-tracked Obligations; this value is side-effected if there is + * an Obligation in it which tracks any expression from the CreatesMustCallFor annotation as + * one of its resource aliases * @param node a method invocation node, invoking a method with a CreatesMustCallFor annotation */ private void checkCreatesMustCallForInvocation( Set obligations, MethodInvocationNode node) { TreePath currentPath = typeFactory.getPath(node.getTree()); - List expressions = + List cmcfExpressions = CreatesMustCallForElementSupplier.getCreatesMustCallForExpressions( node, typeFactory, typeFactory); - Set missing = new HashSet<>(); - for (JavaExpression expression : expressions) { - if (!isValidInvocation(obligations, expression, currentPath)) { + List missing = new ArrayList<>(0); + for (JavaExpression expression : cmcfExpressions) { + if (!isValidCreatesMustCallForExpression(obligations, expression, currentPath)) { missing.add(expression); } } if (missing.isEmpty()) { - // all targets were valid + // All expressions matched one of the rules, so the invocation is valid. return; } String missingStrs = StringsPlume.join(", ", missing); - checker.reportError(node.getTree(), "reset.not.owning", missingStrs); + checker.reportError( + node.getTree(), + "reset.not.owning", + node.getTarget().getMethod().getSimpleName().toString(), + missingStrs); } /** - * An invocation is valid if one of the following conditions is true: 1) the expression is an - * owning pointer, 2) the expression already has a tracked obligation, or 3) the method in which - * the invocation occurs also has an @CreatesMustCallFor annotation, with the same expression. + * Checks the validity of the given expression from an invoked method's {@link + * org.checkerframework.checker.mustcall.qual.CreatesMustCallFor} annotation. Helper method for + * {@link #checkCreatesMustCallForInvocation(Set, MethodInvocationNode)}. * - * @param obligations the currently-tracked obligations; this value is side-effected if it - * contains an expression that is whose must-call obligation is reset + *

An expression is valid if one of the following conditions is true: 1) the expression is an + * owning pointer, 2) the expression already has a tracked Obligation (i.e. there is already a + * resource alias in some Obligation's resource alias set that refers to the expression), or 3) + * the method in which the invocation occurs also has an @CreatesMustCallFor annotation, with the + * same expression. + * + * @param obligations the currently-tracked Obligations; this value is side-effected if there is + * an Obligation in it which tracks {@code expression} as one of its resource aliases * @param expression an element of a method's @CreatesMustCallFor annotation - * @param currentPath the current path - * @return true iff the invocation is valid, as defined above + * @param path the path to the invocation of the method from whose @CreateMustCallFor annotation + * {@code expression} came + * @return true iff the expression is valid, as defined above */ - private boolean isValidInvocation( - Set obligations, JavaExpression expression, TreePath currentPath) { + private boolean isValidCreatesMustCallForExpression( + Set obligations, JavaExpression expression, TreePath path) { if (expression instanceof FieldAccess) { Element elt = ((FieldAccess) expression).getField(); if (!checker.hasOption(MustCallChecker.NO_LIGHTWEIGHT_OWNERSHIP) @@ -320,22 +612,25 @@ private boolean isValidInvocation( Element elt = ((LocalVariable) expression).getElement(); if (!checker.hasOption(MustCallChecker.NO_LIGHTWEIGHT_OWNERSHIP) && typeFactory.getDeclAnnotation(elt, Owning.class) != null) { - // The expression is an Owning param. This satisfies case 1. + // The expression is an Owning formal parameter. Note that this cannot actually + // be a local variable (despite expressions's type being LocalVariable) because + // the @Owning annotation can only be written on methods, parameters, and fields; + // formal parameters are also represented by LocalVariable in the bodies of methods. + // This satisfies case 1. return true; } else { Obligation toRemove = null; Obligation toAdd = null; for (Obligation obligation : obligations) { - for (ResourceAlias alias : obligation.resourceAliases) { - if (expression.equals(alias.reference)) { - // This satisfies case 2 above. Remove all its aliases, then return below. - if (toRemove != null) { - throw new BugInCF( - "tried to remove multiple sets containing a reset expression at once"); - } - toRemove = obligation; - toAdd = new Obligation(ImmutableSet.of(alias)); + ResourceAlias alias = obligation.getResourceAlias(expression); + if (alias != null) { + // This satisfies case 2 above. Remove all its aliases, then return below. + if (toRemove != null) { + throw new TypeSystemError( + "tried to remove multiple sets containing a reset expression at once"); } + toRemove = obligation; + toAdd = new Obligation(ImmutableSet.of(alias)); } } @@ -349,8 +644,8 @@ private boolean isValidInvocation( } // TODO: Getting this every time is inefficient if a method has many @CreatesMustCallFor - // annotations, but that should be a rare path. - MethodTree enclosingMethodTree = TreePathUtil.enclosingMethod(currentPath); + // annotations, but that should be rare. + MethodTree enclosingMethodTree = TreePathUtil.enclosingMethod(path); if (enclosingMethodTree == null) { return false; } @@ -374,7 +669,7 @@ private boolean isValidInvocation( enclosingTarget = null; } - if (representSame(expression, enclosingTarget)) { + if (areSame(expression, enclosingTarget)) { // This satisfies case 3. return true; } @@ -392,7 +687,7 @@ private boolean isValidInvocation( * @param enclosingTarget another, possibly null, JavaExpression * @return true iff they represent the same program element */ - private boolean representSame(JavaExpression target, @Nullable JavaExpression enclosingTarget) { + private boolean areSame(JavaExpression target, @Nullable JavaExpression enclosingTarget) { if (enclosingTarget == null) { return false; } @@ -404,19 +699,18 @@ private boolean representSame(JavaExpression target, @Nullable JavaExpression en } /** - * Given a node representing a method or constructor call, checks that if the result of the call - * has a non-empty {@code @MustCall} type, then the result is pseudo-assigned to some location - * that can take ownership of the result. Searches for the set of same resources in {@code - * obligations} and adds the new resource alias to it if one exists. Otherwise creates a new set. + * Given a node representing a method or constructor call, updates the set of Obligations to + * account for the result, which is treated as a new resource alias. Adds the new resource alias + * to the set of an Obligation in {@code obligations}: either an existing Obligation if the result + * is definitely resource-aliased with it, or a new Obligation if not. * - * @param obligations the currently-tracked obligations. This is always side-effected: an - * obligation is either modified (by removing it from the obligation set and adding a new one) - * to include a new resource alias (the result of the invocation being tracked) or a new - * resource alias set (i.e. obligation) is created and added. - * @param node the node whose result is to be tracked; must be {@link MethodInvocationNode} or - * {@link ObjectCreationNode} + * @param obligations the currently-tracked Obligations. This is always side-effected: either a + * new resource alias is added to the resource alias set of an existing Obligation, or a new + * Obligation with a single-element resource alias set is created and added. + * @param node the invocation node whose result is to be tracked; must be {@link + * MethodInvocationNode} or {@link ObjectCreationNode} */ - private void trackInvocationResult(Set obligations, Node node) { + private void updateObligationsWithInvocationResult(Set obligations, Node node) { Tree tree = node.getTree(); // Only track the result of the call if there is a temporary variable for the call node // (because if there is no temporary, then the invocation must produce an untrackable value, @@ -426,115 +720,146 @@ private void trackInvocationResult(Set obligations, Node node) { return; } - // `mustCallAlias` is the argument passed in the MustCallAlias position, if any exists, - // otherwise null. - Node mustCallAlias = getMustCallAliasArgumentNode(node); - // If mustCallAlias is null and call returns @This, set mustCallAlias to the receiver. - if (mustCallAlias == null - && node instanceof MethodInvocationNode + // `mustCallAliases` is a (possibly-empty) list of arguments passed in a MustCallAlias position. + List mustCallAliases = getMustCallAliasArgumentNodes(node); + // If call returns @This, add the receiver to mustCallAliases. + if (node instanceof MethodInvocationNode && typeFactory.returnsThis((MethodInvocationTree) tree)) { - mustCallAlias = - removeCastsAndGetTmpVarIfPresent(((MethodInvocationNode) node).getTarget().getReceiver()); - } - - ResourceAlias tmpVarAsResourceAlias = new ResourceAlias(new LocalVariable(tmpVar), tree); - if (mustCallAlias instanceof FieldAccessNode) { - // Do not track the call result if the MustCallAlias argument is a field. Handling of - // @Owning fields is a completely separate check, and there is never a need to track an alias - // of - // a non-@Owning field, as by definition such a field does not have obligations! - } else if (mustCallAlias instanceof LocalVariableNode) { - Obligation obligationContainingMustCallAlias = - getObligationForVar(obligations, (LocalVariableNode) mustCallAlias); - // If mustCallAlias is a local variable already being tracked, add tmpVarAsResourceAlias - // to the set containing mustCallAlias. - if (obligationContainingMustCallAlias != null) { - Set newResourceAliasSet = - FluentIterable.from(obligationContainingMustCallAlias.resourceAliases) - .append(tmpVarAsResourceAlias) - .toSet(); - obligations.remove(obligationContainingMustCallAlias); - obligations.add(new Obligation(newResourceAliasSet)); - } - } else { - // If mustCallAlias is neither a field nor a local already in the set of obligations, - // add it to a new set. + mustCallAliases.add( + removeCastsAndGetTmpVarIfPresent( + ((MethodInvocationNode) node).getTarget().getReceiver())); + } + + if (mustCallAliases.isEmpty()) { + // If mustCallAliases is an empty List, add tmpVarAsResourceAlias to a new set. + ResourceAlias tmpVarAsResourceAlias = new ResourceAlias(new LocalVariable(tmpVar), tree); obligations.add(new Obligation(ImmutableSet.of(tmpVarAsResourceAlias))); + } else { + for (Node mustCallAlias : mustCallAliases) { + if (mustCallAlias instanceof FieldAccessNode) { + // Do not track the call result if the MustCallAlias argument is a field. Handling of + // @Owning fields is a completely separate check, and there is never a need to track an + // alias of a non-@Owning field, as by definition such a field does not have must-call + // obligations! + } else if (mustCallAlias instanceof LocalVariableNode) { + // If mustCallAlias is a local variable already being tracked, add tmpVarAsResourceAlias + // to the set containing mustCallAlias. + Obligation obligationContainingMustCallAlias = + getObligationForVar(obligations, (LocalVariableNode) mustCallAlias); + if (obligationContainingMustCallAlias != null) { + ResourceAlias tmpVarAsResourceAlias = + new ResourceAlias( + new LocalVariable(tmpVar), + tree, + obligationContainingMustCallAlias.derivedFromMustCallAlias()); + Set newResourceAliasSet = + FluentIterable.from(obligationContainingMustCallAlias.resourceAliases) + .append(tmpVarAsResourceAlias) + .toSet(); + obligations.remove(obligationContainingMustCallAlias); + obligations.add(new Obligation(newResourceAliasSet)); + // It is not an error if there is no Obligation containing the must-call alias. In that + // case, what has usually happened is that no Obligation was created in the first place. + // For example, when checking the invocation of a "wrapper stream" constructor, if the + // argument in the must-call alias position is some stream with no must-call obligations + // like a ByteArrayInputStream, then no Obligation object will have been created for it + // and therefore obligationContainingMustCallAlias will be null. + } + } + } } } /** * Determines if the result of the given method or constructor invocation node should be tracked * in {@code obligations}. In some cases, there is no need to track the result because the - * obligations are already satisfied in some other way or there cannot possibly be obligations - * because of the structure of the code. + * must-call obligations are already satisfied in some other way or there cannot possibly be + * must-call obligations because of the structure of the code. + * + *

Specifically, an invocation result does NOT need to be tracked if any of the following is + * true: * - *

Specifically, an invocation result does not need to be tracked if any of the following are - * true: (1) the invocation is a call to a {@code this()} or {@code super()} constructor, (2) if - * the method's return type (or, the constructor result, if this is an invocation of a - * constructor) is annotated with MustCallAlias and the argument passed in this invocation in the - * corresponding position is an owning field, or (3) if the method's return type (or the - * constructor result) is non-owning, which can either be because the method has no return type or - * because it is annotated with {@link NotOwning}. + *

    + *
  • The invocation is a call to a {@code this()} or {@code super()} constructor. + *
  • The method's return type is annotated with MustCallAlias and the argument passed in this + * invocation in the corresponding position is an owning field. + *
  • The method's return type is non-owning, which can either be because the method has no + * return type or because the return type is annotated with {@link NotOwning}. + *
* - *

This method can also side-effect obligations, if node is a super or this constructor call - * with MustCallAlias annotations, by removing that obligation. + *

This method can also side-effect {@code obligations}, if node is a super or this constructor + * call with MustCallAlias annotations, by removing that Obligation. * - * @param obligations the current set of obligations + * @param obligations the current set of Obligations, which may be side-effected * @param node the invocation node to check; must be {@link MethodInvocationNode} or {@link * ObjectCreationNode} - * @return true iff the result of node should be tracked in obligations + * @return true iff the result of {@code node} should be tracked in {@code obligations} */ private boolean shouldTrackInvocationResult(Set obligations, Node node) { Tree callTree = node.getTree(); - if (callTree.getKind() == Tree.Kind.METHOD_INVOCATION) { - MethodInvocationTree methodInvokeTree = (MethodInvocationTree) callTree; + if (callTree.getKind() == Tree.Kind.NEW_CLASS) { + // Constructor results from new expressions are always owning. + return true; + } - if (TreeUtils.isSuperConstructorCall(methodInvokeTree) - || TreeUtils.isThisConstructorCall(methodInvokeTree)) { - handleThisOrSuperConstructorMustCallAlias(obligations, node); - return false; + // Now callTree.getKind() == Tree.Kind.METHOD_INVOCATION. + MethodInvocationTree methodInvokeTree = (MethodInvocationTree) callTree; + + if (TreeUtils.isSuperConstructorCall(methodInvokeTree) + || TreeUtils.isThisConstructorCall(methodInvokeTree)) { + List mustCallAliasArguments = getMustCallAliasArgumentNodes(node); + // If there is a MustCallAlias argument that is also in the set of Obligations, then remove + // it; its must-call obligation has been fulfilled by being passed on to the MustCallAlias + // constructor (because a this/super constructor call can only occur in the body of another + // constructor). + for (Node mustCallAliasArgument : mustCallAliasArguments) { + if (mustCallAliasArgument instanceof LocalVariableNode) { + removeObligationsContainingVar(obligations, (LocalVariableNode) mustCallAliasArgument); + } } - return !returnTypeIsMustCallAliasWithUntrackable((MethodInvocationNode) node) - && !hasNotOwningReturnType((MethodInvocationNode) node); + return false; } - return true; + return !returnTypeIsMustCallAliasWithUntrackable((MethodInvocationNode) node) + && !hasNotOwningReturnType((MethodInvocationNode) node); } /** * Returns true if this node represents a method invocation of a must-call-alias method, where the - * other must call alias is untrackable: an owning field or a pointer that is guaranteed to be - * non-owning, such as {@code "this"} or a non-owning field. Owning fields are handled by the rest - * of the checker, not by this algorithm, so they are "untrackable". Non-owning fields and this - * nodes are guaranteed to be non-owning, and therefore do not need to be tracked, either. + * argument in the must-call-alias position is untrackable: an owning field or a pointer that is + * guaranteed to be non-owning, such as {@code "this"} or a non-owning field. Owning fields are + * handled by the rest of the checker, not by this algorithm, so they are "untrackable". + * Non-owning fields and this nodes are guaranteed to be non-owning, and are therefore also + * "untrackable". Because both owning and non-owning fields are untrackable (and there are no + * other kinds of fields), this method returns true for all field accesses. * * @param node a method invocation node * @return true if this is the invocation of a method whose return type is MCA with an owning * field or a definitely non-owning pointer */ private boolean returnTypeIsMustCallAliasWithUntrackable(MethodInvocationNode node) { - Node mustCallAliasArgument = getMustCallAliasArgumentNode(node); - return mustCallAliasArgument instanceof FieldAccessNode - || mustCallAliasArgument instanceof ThisNode; + List mustCallAliasArguments = getMustCallAliasArgumentNodes(node); + for (Node mustCallAliasArg : mustCallAliasArguments) { + if (!(mustCallAliasArg instanceof FieldAccessNode || mustCallAliasArg instanceof ThisNode)) { + return false; + } + } + return !mustCallAliasArguments.isEmpty(); } /** - * Checks if {@code node} is either directly enclosed by a {@link TypeCastNode} or is the then or - * else operand of a {@link TernaryExpressionNode}, by looking at the successor block in the CFG. - * These are all the cases where the enclosing operator is a "no-op" that evaluates to the same - * value as {@code node} (in the appropriate case for a ternary expression). This method is only - * used within {@link #handleSuccessorBlocks(Set, Deque, Set, Block)} to ensure obligations are - * propagated to cast / ternary nodes properly. It relies on the assumption that a {@link - * TypeCastNode} or {@link TernaryExpressionNode} will only appear in a CFG as the first node in a - * block. + * Checks if {@code node} is either directly enclosed by a {@link TypeCastNode}, by looking at the + * successor block in the CFG. In this case the enclosing operator is a "no-op" that evaluates to + * the same value as {@code node}. This method is only used within {@link + * #propagateObligationsToSuccessorBlocks(Set, Block, Set, Deque)} to ensure Obligations are + * propagated to cast nodes properly. It relies on the assumption that a {@link TypeCastNode} will + * only appear in a CFG as the first node in a block. * * @param node the CFG node * @return {@code true} if {@code node} is in a {@link SingleSuccessorBlock} {@code b}, the first - * {@link Node} in {@code b}'s successor block is a {@link TypeCastNode} or a {@link - * TernaryExpressionNode}, and {@code node} is an operand of the successor node; {@code false} - * otherwise + * {@link Node} in {@code b}'s successor block is a {@link TypeCastNode}, and {@code node} is + * an operand of the successor node; {@code false} otherwise */ - private boolean inCastOrTernary(Node node) { + private boolean inCast(Node node) { if (!(node.getBlock() instanceof SingleSuccessorBlock)) { return false; } @@ -545,10 +870,6 @@ private boolean inCastOrTernary(Node node) { Node succNode = succNodes.get(0); if (succNode instanceof TypeCastNode) { return ((TypeCastNode) succNode).getOperand().equals(node); - } else if (succNode instanceof TernaryExpressionNode) { - TernaryExpressionNode ternaryExpressionNode = (TernaryExpressionNode) succNode; - return ternaryExpressionNode.getThenOperand().equals(node) - || ternaryExpressionNode.getElseOperand().equals(node); } } } @@ -556,13 +877,15 @@ private boolean inCastOrTernary(Node node) { } /** - * Transfers ownership of locals to {@code @Owning} parameters at a method or constructor call. + * Transfer ownership of any locals passed as arguments to {@code @Owning} parameters at a method + * or constructor call by removing the Obligations corresponding to those locals. * - * @param obligations the current set of obligations, which is side-effected to remove obligations + * @param obligations the current set of Obligations, which is side-effected to remove Obligations * for locals that are passed as owning parameters to the method or constructor * @param node a method or constructor invocation node */ - private void transferOwnershipToParameters(Set obligations, Node node) { + private void removeObligationsAtOwnershipTransferToParameters( + Set obligations, Node node) { if (checker.hasOption(MustCallChecker.NO_LIGHTWEIGHT_OWNERSHIP)) { // Never transfer ownership to parameters, matching the default in the analysis built into @@ -587,18 +910,23 @@ private void transferOwnershipToParameters(Set obligations, Node nod Node n = removeCastsAndGetTmpVarIfPresent(arguments.get(i)); if (n instanceof LocalVariableNode) { LocalVariableNode local = (LocalVariableNode) n; - if (varTrackedInObligations(local, obligations)) { + if (varTrackedInObligations(obligations, local)) { // check if parameter has an @Owning annotation VariableElement parameter = parameters.get(i); Set annotationMirrors = typeFactory.getDeclAnnotations(parameter); - for (AnnotationMirror anno : annotationMirrors) { if (AnnotationUtils.areSameByName( anno, "org.checkerframework.checker.mustcall.qual.Owning")) { - // transfer ownership! - obligations.remove(getObligationForVar(obligations, local)); - break; + Obligation localObligation = getObligationForVar(obligations, local); + // Passing to an owning parameter is not sufficient to resolve the + // obligation created from a MustCallAlias parameter, because the containing + // method must actually return the value. + if (!localObligation.derivedFromMustCallAlias()) { + // Transfer ownership! + obligations.remove(localObligation); + break; + } } } } @@ -607,15 +935,17 @@ private void transferOwnershipToParameters(Set obligations, Node nod } /** - * If the return type of the enclosing method is {@code @Owning}, transfer ownership of the return - * value and treat its obligations as satisfied by removing it from {@code obligations}. + * If the return type of the enclosing method is {@code @Owning}, treat the must-call obligations + * of the return expression as satisfied by removing all references to them from {@code + * obligations}. * - * @param node a return node + * @param obligations the current set of tracked Obligations. If ownership is transferred, it is + * side-effected to remove any Obligations that are resource-aliased to the return node. * @param cfg the CFG of the enclosing method - * @param obligations the current set of tracked obligations. If ownership is transferred, it is - * side-effected to remove the obligations of the returned value. + * @param node a return node */ - private void handleReturn(ReturnNode node, ControlFlowGraph cfg, Set obligations) { + private void updateObligationsForOwningReturn( + Set obligations, ControlFlowGraph cfg, ReturnNode node) { if (isTransferOwnershipAtReturn(cfg)) { Node returnExpr = node.getResult(); returnExpr = getTempVarOrNode(returnExpr); @@ -627,7 +957,7 @@ private void handleReturn(ReturnNode node, ControlFlowGraph cfg, Set /** * Helper method that gets the temporary node corresponding to {@code node}, if one exists. If - * not, this method just returns its input. + * not, this method returns its input. * * @param node a node * @return the temporary for node, or node if no temporary exists @@ -650,7 +980,7 @@ private Node getTempVarOrNode(final Node node) { */ private boolean isTransferOwnershipAtReturn(ControlFlowGraph cfg) { if (checker.hasOption(MustCallChecker.NO_LIGHTWEIGHT_OWNERSHIP)) { - // If not using LO, default to always transferring at return, just like Eclipse does. + // If not using LO, default to always transfer at return, just like Eclipse does. return true; } @@ -666,14 +996,16 @@ private boolean isTransferOwnershipAtReturn(ControlFlowGraph cfg) { } /** - * Updates a set of obligations to account for an assignment. Assigning to an owning field might - * remove obligations, assigning to a new local variable might modify an obligation (by increasing - * the size of its resource alias set), etc. + * Updates a set of Obligations to account for an assignment. Assigning to an owning field might + * remove Obligations, assigning to a resource variable might remove obligations, assigning to a + * new local variable might modify an Obligation (by increasing the size of its resource alias + * set), etc. * + * @param obligations the set of Obligations to update * @param assignmentNode the assignment - * @param obligations the set of obligations to update */ - private void handleAssignment(AssignmentNode assignmentNode, Set obligations) { + private void updateObligationsForAssignment( + Set obligations, AssignmentNode assignmentNode) { Node lhs = assignmentNode.getTarget(); Element lhsElement = TreeUtils.elementFromTree(lhs.getTree()); // Use the temporary variable for the rhs if it exists. @@ -685,29 +1017,56 @@ private void handleAssignment(AssignmentNode assignmentNode, Set obl boolean isOwningField = !checker.hasOption(MustCallChecker.NO_LIGHTWEIGHT_OWNERSHIP) && typeFactory.getDeclAnnotation(lhsElement, Owning.class) != null; - // Check that there is no obligation on the lhs, if the field is non-final and owning. + // Check that the must-call obligations of the lhs have been satisfied, if the field is + // non-final and owning. if (isOwningField && typeFactory.canCreateObligations() && !ElementUtils.isFinal(lhsElement)) { - checkReassignmentToField(assignmentNode, obligations); + checkReassignmentToField(obligations, assignmentNode); } - // Remove obligations from local variables, now that the owning field is responsible. + // Remove Obligations from local variables, now that the owning field is responsible. // (When obligation creation is turned off, non-final fields cannot take ownership.) if (isOwningField && rhs instanceof LocalVariableNode && (typeFactory.canCreateObligations() || ElementUtils.isFinal(lhsElement))) { - removeObligationsContainingVar(obligations, (LocalVariableNode) rhs); + // Assigning to an owning field is sufficient to clear a must-call alias obligation in + // a constructor. + Element enclosingCtr = lhsElement.getEnclosingElement(); + if (enclosingCtr != null && enclosingCtr.getKind() != ElementKind.CONSTRUCTOR) { + removeObligationsContainingVar(obligations, (LocalVariableNode) rhs); + } else { + removeObligationsContainingVarIfNotDerivedFromMustCallAlias( + obligations, (LocalVariableNode) rhs); + } } + } else if (lhsElement.getKind() == ElementKind.RESOURCE_VARIABLE && isMustCallClose(rhs)) { + removeObligationsContainingVarIfNotDerivedFromMustCallAlias( + obligations, (LocalVariableNode) rhs); } else if (lhs instanceof LocalVariableNode) { LocalVariableNode lhsVar = (LocalVariableNode) lhs; - updateObligationsForPseudoAssignment(assignmentNode, obligations, lhsVar, rhs); + updateObligationsForPseudoAssignment(obligations, assignmentNode, lhsVar, rhs); } } /** - * Remove any obligations that contain {@code var} in their resource-alias set. + * Returns true if must-call type of node only contains close. This is a helper method for + * handling try-with-resources statements. + * + * @param node the node. + * @return true if must-call type of node only contains close. + */ + boolean isMustCallClose(Node node) { + MustCallAnnotatedTypeFactory mcAtf = + typeFactory.getTypeFactoryOfSubchecker(MustCallChecker.class); + AnnotatedTypeMirror mustCallAnnotatedType = mcAtf.getAnnotatedType(node.getTree()); + AnnotationMirror mustCallAnnotation = mustCallAnnotatedType.getAnnotation(MustCall.class); + return typeFactory.getMustCallValues(mcAtf.withoutClose(mustCallAnnotation)).isEmpty(); + } + + /** + * Remove any Obligations that contain {@code var} in their resource-alias set. * - * @param obligations the set of obligations + * @param obligations the set of Obligations * @param var a variable */ private void removeObligationsContainingVar(Set obligations, LocalVariableNode var) { @@ -719,10 +1078,26 @@ private void removeObligationsContainingVar(Set obligations, LocalVa } /** - * Update a set of tracked obligations to account for a (pseudo-)assignment to some variable, as + * Remove any Obligations that contain {@code var} in their resource-alias set, if those resources + * were not derived from an {@link MustCallAlias} parameter. + * + * @param obligations the set of Obligations + * @param var a variable + */ + private void removeObligationsContainingVarIfNotDerivedFromMustCallAlias( + Set obligations, LocalVariableNode var) { + Obligation obligationForVar = getObligationForVar(obligations, var); + while (obligationForVar != null && !obligationForVar.derivedFromMustCallAlias()) { + obligations.remove(obligationForVar); + obligationForVar = getObligationForVar(obligations, var); + } + } + + /** + * Update a set of tracked Obligations to account for a (pseudo-)assignment to some variable, as * in a gen-kill dataflow analysis problem. That is, add ("gen") and remove ("kill") resource - * aliases from obligations in the {@code obligations} set as appropriate based on the - * (pseudo-)assignment performed by {@code node}. This method may also remove an obligation + * aliases from Obligations in the {@code obligations} set as appropriate based on the + * (pseudo-)assignment performed by {@code node}. This method may also remove an Obligation * entirely if the analysis concludes that its resource alias set is empty because the last * tracked alias to it has been overwritten (including checking that the must-call obligations * were satisfied before the assignment). @@ -732,41 +1107,52 @@ private void removeObligationsContainingVar(Set obligations, LocalVa * whose temporary variable is {@code t}, this method may process "assignments" {@code t = x} and * {@code t = y}, thereby capturing the two possible values of {@code t}. * + * @param obligations the tracked Obligations, which will be side-effected * @param node the node performing the pseudo-assignment; it is not necessarily an assignment node - * @param obligations the tracked obligations, which will be side-effected * @param lhsVar the left-hand side variable for the pseudo-assignment * @param rhs the right-hand side for the pseudo-assignment, which must have been converted to a * temporary variable (via a call to {@link - * ResourceLeakAnnotatedTypeFactory#getTempVarForNode(Node)}) + * ResourceLeakAnnotatedTypeFactory#getTempVarForNode}) */ private void updateObligationsForPseudoAssignment( - Node node, Set obligations, LocalVariableNode lhsVar, Node rhs) { - // Replacements to eventually perform in obligations. This map is kept to avoid a + Set obligations, Node node, LocalVariableNode lhsVar, Node rhs) { + // Replacements to eventually perform in Obligations. This map is kept to avoid a // ConcurrentModificationException in the loop below. Map replacements = new LinkedHashMap<>(); - // Construct aliasForAssignment once outside the loop for efficiency. - ResourceAlias aliasForAssignment = new ResourceAlias(new LocalVariable(lhsVar), node.getTree()); + // Cache to re-use on subsequent iterations. + ResourceAlias aliasForAssignment = null; for (Obligation obligation : obligations) { // This is a non-null value iff the resource alias set for obligation needs to // change because of the pseudo-assignment. The value of this variable is the new - // alias set for obligation if it is non-null. + // alias set for `obligation` if it is non-null. Set newResourceAliasesForObligation = null; - // Always kill the lhs var if it is present in the resource alias set for this obligation + // Always kill the lhs var if it is present in the resource alias set for this Obligation // by removing it from the resource alias set. ResourceAlias aliasForLhs = obligation.getResourceAlias(lhsVar); if (aliasForLhs != null) { newResourceAliasesForObligation = new LinkedHashSet<>(obligation.resourceAliases); newResourceAliasesForObligation.remove(aliasForLhs); } - // If rhs is a variable tracked in the obligation's resource alias set, gen the lhs + // If rhs is a variable tracked in the Obligation's resource alias set, gen the lhs // by adding it to the resource alias set. if (rhs instanceof LocalVariableNode - && obligation.hasResourceAlias((LocalVariableNode) rhs)) { + && obligation.canBeSatisfiedThrough((LocalVariableNode) rhs)) { LocalVariableNode rhsVar = (LocalVariableNode) rhs; if (newResourceAliasesForObligation == null) { newResourceAliasesForObligation = new LinkedHashSet<>(obligation.resourceAliases); } + if (aliasForAssignment == null) { + // It is possible to observe assignments to temporary variables, e.g., + // synthetic assignments to ternary expression variables in the CFG. For such + // cases, use the tree associated with the temp var for the resource alias, + // as that is the tree where errors should be reported. + Tree treeForAlias = + typeFactory.isTempVar(lhsVar) + ? typeFactory.getTreeForTempVar(lhsVar) + : node.getTree(); + aliasForAssignment = new ResourceAlias(new LocalVariable(lhsVar), treeForAlias); + } newResourceAliasesForObligation.add(aliasForAssignment); // Remove temp vars from tracking once they are assigned to another location. if (typeFactory.isTempVar(rhsVar)) { @@ -778,7 +1164,7 @@ private void updateObligationsForPseudoAssignment( } // If no changes were made to the resource alias set, there is no need to update the - // obligation. + // Obligation. if (newResourceAliasesForObligation == null) { continue; } @@ -793,14 +1179,16 @@ private void updateObligationsForPseudoAssignment( typeFactory.getStoreBefore(node), mcAtf.getStoreBefore(node), "variable overwritten by assignment " + node.getTree()); + replacements.put(obligation, null); + } else { + replacements.put(obligation, new Obligation(newResourceAliasesForObligation)); } - replacements.put(obligation, new Obligation(newResourceAliasesForObligation)); } - // Finally, update obligations according to the replacements. + // Finally, update the set of Obligations according to the replacements. for (Map.Entry entry : replacements.entrySet()) { obligations.remove(entry.getKey()); - if (!entry.getValue().resourceAliases.isEmpty()) { + if (entry.getValue() != null && !entry.getValue().resourceAliases.isEmpty()) { obligations.add(entry.getValue()); } } @@ -811,16 +1199,16 @@ private void updateObligationsForPseudoAssignment( * re-assignment is valid if the called methods type of the lhs before the assignment satisfies * the must-call obligations of the field. * + * @param obligations current tracked Obligations * @param node an assignment to a non-final, owning field - * @param obligations current tracked obligations */ - private void checkReassignmentToField(AssignmentNode node, Set obligations) { + private void checkReassignmentToField(Set obligations, AssignmentNode node) { Node lhsNode = node.getTarget(); if (!(lhsNode instanceof FieldAccessNode)) { - throw new BugInCF( - "checkReassignmentToField: non-field node " + node + " of type " + node.getClass()); + throw new TypeSystemError( + "checkReassignmentToField: non-field node " + node + " of class " + node.getClass()); } FieldAccessNode lhs = (FieldAccessNode) lhsNode; @@ -835,10 +1223,35 @@ private void checkReassignmentToField(AssignmentNode node, Set oblig MethodTree enclosingMethodTree = TreePathUtil.enclosingMethod(currentPath); if (enclosingMethodTree == null) { - // Assignments outside of methods must be field initializers, which - // are always safe. (Note that the Resource Leak Checker doesn't support static owning fields, - // so static initializers don't need to be considered here.) - return; + // If the assignment is taking place outside of a method, the Resource Leak Checker + // issues an error unless it can prove that the assignment is a field initializer, which + // are always safe. The node's TreeKind being "VARAIBLE" is a safe proxy for this requirement, + // because VARIABLE Trees are only used for declarations. An assignment to a field that is + // also a declaration must be a field initializer. + if (node.getTree().getKind() == Tree.Kind.VARIABLE) { + return; + } else { + // Issue an error if the field has a non-empty must-call type. + MustCallAnnotatedTypeFactory mcTypeFactory = + typeFactory.getTypeFactoryOfSubchecker(MustCallChecker.class); + AnnotationMirror mcAnno = + mcTypeFactory.getAnnotatedType(lhs.getElement()).getAnnotation(MustCall.class); + List mcValues = + AnnotationUtils.getElementValueArray( + mcAnno, mcTypeFactory.getMustCallValueElement(), String.class); + if (mcValues.isEmpty()) { + return; + } + Element lhsElement = TreeUtils.elementFromTree(lhs.getTree()); + checker.reportError( + node.getTree(), + "required.method.not.called", + formatMissingMustCallMethods(mcValues), + "field " + lhsElement.getSimpleName().toString(), + lhsElement.asType().toString(), + "Field assignment outside method or declaration might overwrite field's current value"); + return; + } } // Check that there is a corresponding CreatesMustCallFor annotation, unless this is @@ -846,7 +1259,7 @@ private void checkReassignmentToField(AssignmentNode node, Set oblig // extend beyond the method's body (and which therefore could not be targeted by an annotation // on the method declaration), or 2) the rhs is a null literal (so there's nothing to reset). if (!(receiver instanceof LocalVariableNode - && varTrackedInObligations((LocalVariableNode) receiver, obligations)) + && varTrackedInObligations(obligations, (LocalVariableNode) receiver)) && !(node.getExpression() instanceof NullLiteralNode)) { checkEnclosingMethodIsCreatesMustCallFor(node, enclosingMethodTree); } @@ -899,6 +1312,7 @@ && varTrackedInObligations((LocalVariableNode) receiver, obligations)) node.getTree(), "required.method.not.called", formatMissingMustCallMethods(mcValues), + "field " + lhsElement.getSimpleName().toString(), lhsElement.asType().toString(), " Non-final owning field might be overwritten"); } @@ -936,6 +1350,7 @@ private void checkEnclosingMethodIsCreatesMustCallFor( checker.reportError( enclosingMethod, "missing.creates.mustcall.for", + enclosingMethodElt.getSimpleName().toString(), receiverString, ((FieldAccessNode) lhs).getFieldName()); return; @@ -961,6 +1376,7 @@ private void checkEnclosingMethodIsCreatesMustCallFor( checker.reportError( enclosingMethod, "incompatible.creates.mustcall.for", + enclosingMethodElt.getSimpleName().toString(), receiverString, ((FieldAccessNode) lhs).getFieldName(), String.join(", ", checked)); @@ -981,43 +1397,43 @@ private String receiverAsString(FieldAccessNode fieldAccessNode) { return ((LocalVariableNode) receiver).getName(); } - throw new BugInCF( + throw new TypeSystemError( "unexpected receiver of field assignment: " + receiver + " of type " + receiver.getClass()); } /** - * Finds the argument passed in the {@code @MustCallAlias} position for a call. + * Finds the arguments passed in the {@code @MustCallAlias} positions for a call. * * @param callNode callNode representing the call; must be {@link MethodInvocationNode} or {@link * ObjectCreationNode} * @return if {@code callNode} invokes a method with a {@code @MustCallAlias} annotation on some - * formal parameter (or the receiver), returns the result of calling {@link - * #removeCastsAndGetTmpVarIfPresent(Node)} on the argument passed in that position. - * Otherwise, returns {@code null}. + * formal parameter(s) (or the receiver), returns the result of calling {@link + * #removeCastsAndGetTmpVarIfPresent(Node)} on the argument(s) passed in corresponding + * position(s). Otherwise, returns an empty List. */ - private @Nullable Node getMustCallAliasArgumentNode(Node callNode) { + private List getMustCallAliasArgumentNodes(Node callNode) { Preconditions.checkArgument( callNode instanceof MethodInvocationNode || callNode instanceof ObjectCreationNode); + List result = new ArrayList<>(); if (!typeFactory.hasMustCallAlias(callNode.getTree())) { - return null; + return result; } - Node result = null; List args = getArgumentsOfInvocation(callNode); List parameters = getParametersOfInvocation(callNode); for (int i = 0; i < args.size(); i++) { if (typeFactory.hasMustCallAlias(parameters.get(i))) { - result = args.get(i); - break; + result.add(removeCastsAndGetTmpVarIfPresent(args.get(i))); } } // If none of the parameters were @MustCallAlias, it must be the receiver - if (result == null && callNode instanceof MethodInvocationNode) { - result = ((MethodInvocationNode) callNode).getTarget().getReceiver(); + if (result.isEmpty() && callNode instanceof MethodInvocationNode) { + result.add( + removeCastsAndGetTmpVarIfPresent( + ((MethodInvocationNode) callNode).getTarget().getReceiver())); } - result = removeCastsAndGetTmpVarIfPresent(result); return result; } @@ -1049,7 +1465,7 @@ private List getArgumentsOfInvocation(Node node) { } else if (node instanceof ObjectCreationNode) { return ((ObjectCreationNode) node).getArguments(); } else { - throw new BugInCF("unexpected node type " + node.getClass()); + throw new TypeSystemError("unexpected node type " + node.getClass()); } } @@ -1069,7 +1485,7 @@ private List getParametersOfInvocation(Node node) { } else if (node instanceof ObjectCreationNode) { executableElement = TreeUtils.elementFromUse(((ObjectCreationNode) node).getTree()); } else { - throw new BugInCF("unexpected node type " + node.getClass()); + throw new TypeSystemError("unexpected node type " + node.getClass()); } return executableElement.getParameters(); @@ -1133,48 +1549,46 @@ private boolean hasNotOwningReturnType(MethodInvocationNode node) { } /** - * Propagates a set of obligations to successors, and performs consistency checks when variables + * Propagates a set of Obligations to successors, and performs consistency checks when variables * are going out of scope. * *

The basic algorithm loops over the successor blocks of the current block. For each - * successor, it checks every obligation in obligations. If the successor is an exit block or all - * of an obligation's resource aliases might be going out of scope, then a consistency check + * successor, it checks every Obligation in obligations. If the successor is an exit block or all + * of an Obligation's resource aliases might be going out of scope, then a consistency check * occurs (with two exceptions, both related to temporary variables that don't actually get * assigned; see code comments for details) and an error is issued if it fails. If the successor - * is any other kind of block and there is information about at least one of the obligation's + * is any other kind of block and there is information about at least one of the Obligation's * aliases in the successor store (i.e. the resource itself definitely does not go out of scope), - * then the obligation is passed forward to the successor ("propagated") with any definitely + * then the Obligation is passed forward to the successor ("propagated") with any definitely * out-of-scope aliases removed from its resource alias set. * - * @param visited block-obligations pairs already analyzed or already on the worklist - * @param worklist current worklist - * @param obligations obligations of the current block + * @param obligations Obligations for the current block * @param currentBlock the current block + * @param visited block-Obligations pairs already analyzed or already on the worklist + * @param worklist current worklist */ - private void handleSuccessorBlocks( - Set visited, - Deque worklist, + private void propagateObligationsToSuccessorBlocks( Set obligations, - Block currentBlock) { + Block currentBlock, + Set visited, + Deque worklist) { List currentBlockNodes = currentBlock.getNodes(); // For each successor block that isn't caused by an ignored exception type, this loop computes - // the set of obligations that should be propagated to it and then adds it to the worklist if + // the set of Obligations that should be propagated to it and then adds it to the worklist if // any of its resource aliases are still in scope in the successor block. If none are, then the - // loop performs a consistency check for that obligation. + // loop performs a consistency check for that Obligation. for (Pair successorAndExceptionType : getSuccessorsExceptIgnoredExceptions(currentBlock)) { Block successor = successorAndExceptionType.first; // If nonnull, currentBlock is an ExceptionBlock. TypeMirror exceptionType = successorAndExceptionType.second; - Set curObligations = - handleTernarySuccIfNeeded(currentBlock, successor, obligations); - // successorObligations eventually contains the obligations to propagate to successor. The + // successorObligations eventually contains the Obligations to propagate to successor. The // loop below mutates it. Set successorObligations = new LinkedHashSet<>(); - // A detailed reason to give in the case that the last resource alias of an obligation - // goes out of scope without a called-methods type that satisfies the obligation along the - // current control-flow edge. Computed here for efficiency; used in the loop over the - // obligations, below. + // A detailed reason to give in the case that the last resource alias of an Obligation + // goes out of scope without a called-methods type that satisfies the corresponding + // must-call obligation along the current control-flow edge. Computed here for efficiency; + // used in the loop over the Obligations, below. String exitReasonForErrorMessage = exceptionType == null ? @@ -1185,10 +1599,10 @@ private void handleSuccessorBlocks( + ((ExceptionBlock) currentBlock).getNode().getTree() + " with exception type " + exceptionType; - // Computed outside the obligation loop for efficiency. + // Computed outside the Obligation loop for efficiency. CFStore regularStoreOfSuccessor = analysis.getInput(successor).getRegularStore(); - for (Obligation obligation : curObligations) { - // This boolean is true if there is no evidence that the obligation does not go out of + for (Obligation obligation : obligations) { + // This boolean is true if there is no evidence that the Obligation does not go out of // scope - that is, if there is definitely a resource alias that is in scope in the // successor. boolean obligationGoesOutOfScopeBeforeSuccessor = true; @@ -1198,7 +1612,7 @@ private void handleSuccessorBlocks( break; } } - // This check is to determine if this obligation's resource aliases are definitely going + // This check is to determine if this Obligation's resource aliases are definitely going // out of scope: if this is an exit block or there is no information about any of them in // the successor store, all aliases must be going out of scope and a consistency check // should occur. @@ -1207,8 +1621,8 @@ private void handleSuccessorBlocks( MustCallAnnotatedTypeFactory mcAtf = typeFactory.getTypeFactoryOfSubchecker(MustCallChecker.class); - // If successor is an exceptional successor, and obligation represents the temporary - // variable for currentBlock's node, do not propagate or do a consistency check, as in + // If successor is an exceptional successor, and Obligation represents the temporary + // variable for currentBlock's node, do not propagate or do a consistency check, as in // the exceptional case the "assignment" to the temporary variable does not succeed. // // Note that this test cannot be "successor.getType() == BlockType.EXCEPTIONAL_BLOCK", @@ -1222,24 +1636,35 @@ private void handleSuccessorBlocks( LocalVariableNode tmpVarForExcNode = typeFactory.getTempVarForNode(exceptionalNode); if (tmpVarForExcNode != null && obligation.resourceAliases.size() == 1 - && obligation.hasResourceAlias(tmpVarForExcNode)) { + && obligation.canBeSatisfiedThrough(tmpVarForExcNode)) { continue; } } - // Always propagate obligation to successor if current block represents code nested - // in a cast or ternary expression. Without this logic, the analysis may report a false - // positive when the obligation represents a temporary variable for a nested + // Always propagate the Obligation to the successor if current block represents code + // nested + // in a cast. Without this logic, the analysis may report a false + // positive when the Obligation represents a temporary variable for a nested // expression, as the temporary may not appear in the successor store and hence seems to // be going out of scope. The temporary will be handled with special logic; casts are - // unwrapped at various points in the analysis, and ternary expressions are handled by - // handleTernarySuccIfNeeded. - if (currentBlockNodes.size() == 1 && inCastOrTernary(currentBlockNodes.get(0))) { + // unwrapped at various points in the analysis. + if (currentBlockNodes.size() == 1 && inCast(currentBlockNodes.get(0))) { successorObligations.add(obligation); continue; } - // At this point, a consistency check will definitely occur. + // At this point, a consistency check will definitely occur, unless the obligation + // was derived from a MustCallAlias parameter. If it was, an error is immediately + // issued, because such a parameter should not go out of scope without its obligation + // being resolved some other way. + if (obligation.derivedFromMustCallAlias()) { + checker.reportError( + obligation.resourceAliases.asList().get(0).tree, + "mustcallalias.out.of.scope", + exitReasonForErrorMessage); + continue; + } + // Which stores from the called-methods and must-call checkers are used in // the consistency check varies depending on the context. The rules are: // 1. if the current block has no nodes (and therefore the store must come from a block @@ -1256,8 +1681,8 @@ private void handleSuccessorBlocks( // method that might throw an exception, and the consistency check is for // an exceptional path, use the MC store immediately before the method invocation, // because the method threw an exception rather than finishing and therefore did - // not actually create an obligation, so the MC store after might contain - // obligations that do not need to be fulfilled along this path. + // not actually create any must-call obligation, so the MC store after might + // contain must-call obligations that do not need to be fulfilled along this path. // 2b. in all other cases, use the MC store from after the last node in the block. CFStore mcStore, cmStore; if (currentBlockNodes.size() == 0 /* currentBlock is special or conditional */) { @@ -1285,9 +1710,9 @@ private void handleSuccessorBlocks( } checkMustCall(obligation, cmStore, mcStore, exitReasonForErrorMessage); - } else { // In this case, there is info in the successor store about some alias in - // obligation. Handles the possibility that some resource in the obligation may go out of - // scope. + } else { + // In this case, there is info in the successor store about some alias in the Obligation. + // Handles the possibility that some resource in the Obligation may go out of scope. Set copyOfResourceAliases = new LinkedHashSet<>(obligation.resourceAliases); copyOfResourceAliases.removeIf( @@ -1302,11 +1727,7 @@ private void handleSuccessorBlocks( /** * Returns true if {@code alias.reference} is definitely in-scope in the successor store: that is, - * there is a value for it in {@code successorStore}. Also returns true if {@code alias.tree} is a - * {@link ConditionalExpressionTree}. This special rule for a {@link ConditionalExpressionTree} is - * to accommodate the handling of ternary expressions, because the analysis tracks the temporary - * variable for the expression at the program point before that expression; see {@link - * #handleTernarySuccIfNeeded(Block, Block, Set)}. + * there is a value for it in {@code successorStore}. * * @param successorStore the regular store of the successor block * @param alias the resource alias to check @@ -1314,56 +1735,7 @@ private void handleSuccessorBlocks( * checking algorithm in the successor block from which the store came */ private boolean aliasInScopeInSuccessor(CFStore successorStore, ResourceAlias alias) { - return successorStore.getValue(alias.reference) != null - || alias.tree instanceof ConditionalExpressionTree; - } - - /** - * Handles control-flow to a block starting with a {@link TernaryExpressionNode}. - * - *

In the Checker Framework's CFG, a ternary expression is represented as a {@link - * org.checkerframework.dataflow.cfg.block.ConditionalBlock}, whose successor blocks are the two - * cases of the ternary expression. The {@link TernaryExpressionNode} is the first node in the - * successor block of the two cases. - * - *

To handle this representation, the control-flow transition from a node for a ternary - * expression case c to the successor {@link TernaryExpressionNode} t is treated - * as a pseudo-assignment from c to the temporary variable for t. With this - * handling, the obligations reaching the successor node of t will properly account for - * the execution of case c. - * - *

If the successor block does not begin with a {@link TernaryExpressionNode} that needs to be - * handled, this method simply returns {@code obligations}. - * - * @param pred the predecessor block, potentially corresponding to the ternary expression case - * @param succ the successor block, potentially starting with a {@link TernaryExpressionNode} - * @param obligations the obligations before the control-flow transition - * @return a new set of obligations to account for the {@link TernaryExpressionNode}, or just - * {@code obligations} if no handling is required. - */ - private Set handleTernarySuccIfNeeded( - Block pred, Block succ, Set obligations) { - List succNodes = succ.getNodes(); - if (succNodes.isEmpty() || !(succNodes.get(0) instanceof TernaryExpressionNode)) { - return obligations; - } - TernaryExpressionNode ternaryNode = (TernaryExpressionNode) succNodes.get(0); - LocalVariableNode ternaryTempVar = typeFactory.getTempVarForNode(ternaryNode); - if (ternaryTempVar == null) { - return obligations; - } - List predNodes = pred.getNodes(); - // Right-hand side of the pseudo-assignment to the ternary expression temporary variable. - Node rhs = NodeUtils.removeCasts(predNodes.get(predNodes.size() - 1)); - if (!(rhs instanceof LocalVariableNode)) { - rhs = typeFactory.getTempVarForNode(rhs); - if (rhs == null) { - return obligations; - } - } - Set newDefs = new LinkedHashSet<>(obligations); - updateObligationsForPseudoAssignment(ternaryNode, newDefs, ternaryTempVar, rhs); - return newDefs; + return successorStore.getValue(alias.reference) != null; } /** @@ -1395,13 +1767,16 @@ private Set computeOwningParameters(ControlFlowGraph cfg) { Set result = new LinkedHashSet<>(1); for (VariableTree param : method.getParameters()) { Element paramElement = TreeUtils.elementFromDeclaration(param); - if (typeFactory.hasMustCallAlias(paramElement) - || (typeFactory.hasDeclaredMustCall(param) + boolean hasMustCallAlias = typeFactory.hasMustCallAlias(paramElement); + if (hasMustCallAlias + || (typeFactory.declaredTypeHasMustCall(param) && !checker.hasOption(MustCallChecker.NO_LIGHTWEIGHT_OWNERSHIP) && paramElement.getAnnotation(Owning.class) != null)) { result.add( new Obligation( - ImmutableSet.of(new ResourceAlias(new LocalVariable(paramElement), param)))); + ImmutableSet.of( + new ResourceAlias( + new LocalVariable(paramElement), param, hasMustCallAlias)))); // Increment numMustCall for each @Owning parameter tracked by the enclosing method. incrementNumMustCall(paramElement); } @@ -1415,14 +1790,14 @@ private Set computeOwningParameters(ControlFlowGraph cfg) { * Checks whether there is some resource alias set R in {@code obligations} such that * R contains a {@link ResourceAlias} whose local variable is {@code node}. * + * @param obligations the set of Obligations to search * @param var the local variable to look for - * @param obligations the set of obligations to search - * @return true iff there is a resource alias set in obligations that contains node + * @return true iff there is a resource alias set in {@code obligations} that contains node */ private static boolean varTrackedInObligations( - LocalVariableNode var, Set obligations) { + Set obligations, LocalVariableNode var) { for (Obligation obligation : obligations) { - if (obligation.hasResourceAlias(var)) { + if (obligation.canBeSatisfiedThrough(var)) { return true; } } @@ -1430,18 +1805,18 @@ private static boolean varTrackedInObligations( } /** - * Gets the obligation whose resource aliase set contains the given local variable, if one exists - * in obligations. + * Gets the Obligation whose resource aliase set contains the given local variable, if one exists + * in {@code obligations}. * - * @param obligations set of obligations + * @param obligations set of Obligations * @param node variable of interest - * @return the obligation in {@code obligations} whose resource alias set contains {@code node}, - * or {@code null} if there is no such obligation + * @return the Obligation in {@code obligations} whose resource alias set contains {@code node}, + * or {@code null} if there is no such Obligation */ private static @Nullable Obligation getObligationForVar( Set obligations, LocalVariableNode node) { for (Obligation obligation : obligations) { - if (obligation.hasResourceAlias(node)) { + if (obligation.canBeSatisfiedThrough(node)) { return obligation; } } @@ -1449,11 +1824,11 @@ private static boolean varTrackedInObligations( } /** - * For the given obligation, checks that at least one of its variables has its {@code @MustCall} + * For the given Obligation, checks that at least one of its variables has its {@code @MustCall} * obligation satisfied, based on {@code @CalledMethods} and {@code @MustCall} types in the given * stores. * - * @param obligation the obligation + * @param obligation the Obligation * @param cmStore the called-methods store * @param mcStore the must-call store * @param outOfScopeReason if the {@code @MustCall} obligation is not satisfied, a useful @@ -1462,7 +1837,7 @@ private static boolean varTrackedInObligations( private void checkMustCall( Obligation obligation, CFStore cmStore, CFStore mcStore, String outOfScopeReason) { - List mustCallValue = typeFactory.getMustCallValue(obligation.resourceAliases, mcStore); + List mustCallValue = obligation.getMustCallMethods(typeFactory, mcStore); // optimization: if there are no must-call methods, do not need to perform the check if (mustCallValue == null || mustCallValue.isEmpty()) { return; @@ -1473,11 +1848,11 @@ private void checkMustCall( // sometimes the store is null! this looks like a bug in checker dataflow. // TODO track down and report the root-cause bug - CFValue lhsCFValue = cmStore != null ? cmStore.getValue(alias.reference) : null; + CFValue aliasCFValue = cmStore != null ? cmStore.getValue(alias.reference) : null; AnnotationMirror cmAnno = null; - if (lhsCFValue != null) { // When store contains the lhs - for (AnnotationMirror anno : lhsCFValue.getAnnotations()) { + if (aliasCFValue != null) { // When store contains the lhs + for (AnnotationMirror anno : aliasCFValue.getAnnotations()) { if (AnnotationUtils.areSameByName( anno, "org.checkerframework.checker.calledmethods.qual.CalledMethods")) { cmAnno = anno; @@ -1488,7 +1863,7 @@ private void checkMustCall( cmAnno = typeFactory .getAnnotatedType(alias.reference.getElement()) - .getAnnotationInHierarchy(typeFactory.top); + .getEffectiveAnnotationInHierarchy(typeFactory.top); } if (calledMethodsSatisfyMustCall(mustCallValue, cmAnno)) { @@ -1507,6 +1882,7 @@ private void checkMustCall( firstAlias.tree, "required.method.not.called", formatMissingMustCallMethods(mustCallValue), + firstAlias.reference.toString(), firstAlias.reference.getType().toString(), outOfScopeReason); } @@ -1586,13 +1962,14 @@ private boolean calledMethodsSatisfyMustCall( * along an exceptional path. These kinds of errors fall into a few categories: runtime errors, * errors that the JVM can issue on any statement, and errors that can be prevented by running * some other CF checker. + * + *

Package-private to permit access from {@link ResourceLeakAnalysis}. */ - private static Set ignoredExceptionTypes = + /* package-private */ static final Set ignoredExceptionTypes = new HashSet<>( ImmutableSet.of( // Any method call has a CFG edge for Throwable/RuntimeException/Error to represent - // run-time - // misbehavior. Ignore it. + // run-time misbehavior. Ignore it. Throwable.class.getCanonicalName(), Error.class.getCanonicalName(), RuntimeException.class.getCanonicalName(), @@ -1659,7 +2036,7 @@ private static void propagate( static String formatMissingMustCallMethods(List mustCallVal) { int size = mustCallVal.size(); if (size == 0) { - throw new BugInCF("empty mustCallVal " + mustCallVal); + throw new TypeSystemError("empty mustCallVal " + mustCallVal); } else if (size == 1) { return "method " + mustCallVal.get(0); } else { @@ -1668,24 +2045,24 @@ static String formatMissingMustCallMethods(List mustCallVal) { } /** - * A pair of a {@link Block} and a set of obligations (i.e. dataflow facts) on entry to the block. - * Each obligation is an {@link Obligation}, representing a set of resource aliases for some - * tracked resource. The analyzer's worklist consists of BlockWithObligations objects, each - * representing the need to handle the set of obligations reaching the block during analysis. + * A pair of a {@link Block} and a set of dataflow facts on entry to the block. Each dataflow fact + * represents a set of resource aliases for some tracked resource. The analyzer's worklist + * consists of BlockWithObligations objects, each representing the need to handle the set of + * dataflow facts reaching the block during analysis. */ private static class BlockWithObligations { /** The block. */ public final Block block; - /** The facts. */ + /** The dataflow facts. */ public final ImmutableSet obligations; /** - * Create a new BlockWithObligations from a block and a set of obligations. + * Create a new BlockWithObligations from a block and a set of dataflow facts. * * @param b the block - * @param obligations the set of incoming obligations at the start of the block (may be the + * @param obligations the set of incoming Obligations at the start of the block (may be the * empty set) */ public BlockWithObligations(Block b, Set obligations) { @@ -1710,135 +2087,4 @@ public int hashCode() { return Objects.hash(block, obligations); } } - - /** - * An Obligation is a set of resource aliases whose must-call obligations can all be fulfilled by - * calling the required method(s) on any of the resource aliases. {@link - * MustCallConsistencyAnalyzer} is a dataflow analysis whose dataflow facts are Obligations. - */ - private static class Obligation { - - /** - * The set of resource aliases that can satisfy this obligation. {@code Obligation} is deeply - * immutable. If some code were to accidentally mutate a {@code resourceAliases} set it could be - * really nasty to debug, so this set is always immutable. - */ - public final ImmutableSet resourceAliases; - - /** - * Create an obligation from a set of resource aliases. - * - * @param resourceAliases a set of resource aliases - */ - public Obligation(Set resourceAliases) { - this.resourceAliases = ImmutableSet.copyOf(resourceAliases); - } - - /** - * Returns the resource alias corresponding to {@code localVariableNode} if one is present. - * Otherwise, returns null. - * - * @param localVariableNode some local variable - * @return the resource alias corresponding to {@code localVariableNode} if one is present; - * otherwise, null - */ - private @Nullable ResourceAlias getResourceAlias(LocalVariableNode localVariableNode) { - Element element = localVariableNode.getElement(); - for (ResourceAlias alias : resourceAliases) { - if (alias.reference.getElement().equals(element)) { - return alias; - } - } - return null; - } - - /** - * Returns true if the resource alias corresponding to {@code localVariableNode} is present. - * Otherwise, returns false. - * - * @param localVariableNode some local variable node - * @return true if the resource alias corresponding to {@code localVariableNode} is present; - * otherwise, false - */ - private boolean hasResourceAlias(LocalVariableNode localVariableNode) { - return getResourceAlias(localVariableNode) != null; - } - - @Override - public String toString() { - return "Obligation: resourceAliases: " + Iterables.toString(resourceAliases); - } - - @Override - public boolean equals(Object obj) { - if (this == obj) { - return true; - } - if (obj == null || getClass() != obj.getClass()) { - return false; - } - Obligation that = (Obligation) obj; - return this.resourceAliases.equals(that.resourceAliases); - } - - @Override - public int hashCode() { - return Objects.hash(resourceAliases); - } - } - - /** - * This class represents a single resource alias in a resource alias set. - * - *

Internally, a resource alias is represented by a pair of a local or temporary variable (the - * "reference" through which the must-call obligations for the alias set can be satisfied) along - * with a tree in the corresponding method that "assigns" the variable. Besides a normal - * assignment, the tree may be a {@link VariableTree} in the case of a formal parameter. The tree - * is only used for error-reporting purposes. - */ - /* package-private */ static class ResourceAlias { - - /** - * The JavaExpression that represents the reference through which this resource can have its - * must-call obligations satisfied. This is either a local variable actually defined in the - * source code, or a temporary variable for an expression. - */ - public final LocalVariable reference; - - /** The tree at which it was assigned, for error reporting. */ - public final Tree tree; - - /** - * Create a new resource alias. - * - * @param reference the local variable - * @param tree the tree - */ - public ResourceAlias(LocalVariable reference, Tree tree) { - this.reference = reference; - this.tree = tree; - } - - @Override - public String toString() { - return "(ResourceAlias: reference: " + reference + " |||| tree: " + tree + ")"; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - ResourceAlias that = (ResourceAlias) o; - return reference.equals(that.reference) && tree.equals(that.tree); - } - - @Override - public int hashCode() { - return Objects.hash(reference, tree); - } - } } diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallInferenceLogic.java b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallInferenceLogic.java new file mode 100644 index 0000000000..2022418fc2 --- /dev/null +++ b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallInferenceLogic.java @@ -0,0 +1,174 @@ +package org.checkerframework.checker.resourceleak; + +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Deque; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.Element; +import org.checkerframework.checker.mustcall.qual.Owning; +import org.checkerframework.common.wholeprograminference.WholeProgramInference; +import org.checkerframework.dataflow.cfg.ControlFlowGraph; +import org.checkerframework.dataflow.cfg.block.Block; +import org.checkerframework.dataflow.cfg.block.ConditionalBlock; +import org.checkerframework.dataflow.cfg.block.SingleSuccessorBlock; +import org.checkerframework.dataflow.cfg.node.MethodInvocationNode; +import org.checkerframework.dataflow.cfg.node.Node; +import org.checkerframework.javacutil.AnnotationBuilder; +import org.checkerframework.javacutil.BugInCF; +import org.checkerframework.javacutil.TreeUtils; + +/** + * This class contains the Resource Leak Checker's annotation inference algorithm. It contains + * inference logic for owning annotations on final owning fields. It adds an @Owning annotation on a + * field if it finds a method that satisfies the @MustCall obligation of the field along some path + * to the regular exit point. + */ +public class MustCallInferenceLogic { + + /** The set of owning fields. */ + private Set owningFields = new HashSet<>(); + + /** + * The type factory for the Resource Leak Checker, which is used to access the Must Call Checker. + */ + private final ResourceLeakAnnotatedTypeFactory typeFactory; + + /** The {@link Owning} annotation. */ + protected final AnnotationMirror OWNING; + + /** The control flow graph. */ + private ControlFlowGraph cfg; + + /** + * Creates a MustCallInferenceLogic. If the type factory has whole program inference enabled, its + * postAnalyze method should instantiate a new MustCallInferenceLogic using this constructor and + * then call {@link #runInference()}. + * + * @param typeFactory the type factory + * @param cfg the ControlFlowGraph + */ + MustCallInferenceLogic(ResourceLeakAnnotatedTypeFactory typeFactory, ControlFlowGraph cfg) { + this.typeFactory = typeFactory; + this.cfg = cfg; + OWNING = AnnotationBuilder.fromClass(this.typeFactory.getElementUtils(), Owning.class); + } + + /** + * Runs the inference algorithm on the contents of the {@link #cfg} field. + * + *

Operationally, it checks method invocations for fields with non-empty @MustCall obligations + * along all paths to the regular exit point in the method body of the method represented by + * {@link #cfg}, and updates the {@link #owningFields} set if it discovers an owning field whose + * must-call obligations were satisfied along one of the checked paths. + */ + void runInference() { + Set visited = new HashSet<>(); + Deque worklist = new ArrayDeque<>(); + Block entry = this.cfg.getEntryBlock(); + + worklist.add(entry); + visited.add(entry); + + while (!worklist.isEmpty()) { + Block current = worklist.remove(); + + for (Node node : current.getNodes()) { + if (node instanceof MethodInvocationNode) { + checkForMustCallInvocationOnField((MethodInvocationNode) node); + } + } + + propagateRegPaths(current, visited, worklist); + } + } + + /** + * If the receiver of {@code mNode} is a candidate owning field and the method invocation + * satisfies the field's must-call obligation, then adds that field to the {@link #owningFields} + * set. + * + * @param mNode the MethodInvocationNode + */ + private void checkForMustCallInvocationOnField(MethodInvocationNode mNode) { + Node receiver = mNode.getTarget().getReceiver(); + if (receiver.getTree() == null) { + return; + } + + Element receiverEl = TreeUtils.elementFromTree(receiver.getTree()); + + if (receiverEl != null && typeFactory.isCandidateOwningField(receiverEl)) { + Element method = TreeUtils.elementFromTree(mNode.getTree()); + List mustCallValues = typeFactory.getMustCallValue(receiverEl); + + // This assumes that any MustCall annotation has at most one element. + // TODO: generalize this to MustCall annotations with more than one element. + if (mustCallValues.size() == 1 + && mustCallValues.contains(method.getSimpleName().toString())) { + owningFields.add(receiverEl); + } + } + } + + /** + * Updates {@code worklist} with the next block along all paths to the regular exit point. If the + * next block is a regular exit point, adds an {@literal @}Owning annotation for fields in {@link + * #owningFields}. + * + * @param curBlock the current block + * @param visited set of blocks already on the worklist + * @param worklist current worklist + */ + private void propagateRegPaths(Block curBlock, Set visited, Deque worklist) { + + List successors = getNormalSuccessors(curBlock); + + for (Block b : successors) { + // If b is a special block, it must be the regular exit, since we do not propagate to + // exceptional successors. + if (b.getType() == Block.BlockType.SPECIAL_BLOCK) { + WholeProgramInference wpi = typeFactory.getWholeProgramInference(); + assert wpi != null : "MustCallInference is running without WPI."; + for (Element fieldElt : owningFields) { + wpi.addFieldDeclarationAnnotation(fieldElt, OWNING); + } + } + + if (visited.add(b)) { + worklist.add(b); + } + } + } + + /** + * Returns the non-exceptional successors of the current block. + * + * @param cur the current block + * @return the successors of this current block + */ + private List getNormalSuccessors(Block cur) { + List successorBlock = new ArrayList<>(); + + if (cur.getType() == Block.BlockType.CONDITIONAL_BLOCK) { + + ConditionalBlock ccur = (ConditionalBlock) cur; + + successorBlock.add(ccur.getThenSuccessor()); + successorBlock.add(ccur.getElseSuccessor()); + + } else { + if (!(cur instanceof SingleSuccessorBlock)) { + throw new BugInCF("BlockImpl is neither a conditional block nor a SingleSuccessorBlock"); + } + + Block b = ((SingleSuccessorBlock) cur).getSuccessor(); + if (b != null) { + successorBlock.add(b); + } + } + return successorBlock; + } +} diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakAnalysis.java b/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakAnalysis.java new file mode 100644 index 0000000000..8de3ec4142 --- /dev/null +++ b/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakAnalysis.java @@ -0,0 +1,24 @@ +package org.checkerframework.checker.resourceleak; + +import org.checkerframework.checker.calledmethods.CalledMethodsAnalysis; +import org.checkerframework.checker.calledmethods.CalledMethodsAnnotatedTypeFactory; +import org.checkerframework.common.basetype.BaseTypeChecker; + +/** + * This variant of CFAnalysis extends the set of ignored exception types to include all those + * ignored by the {@link MustCallConsistencyAnalyzer}. See {@link + * MustCallConsistencyAnalyzer#ignoredExceptionTypes}. + */ +public class ResourceLeakAnalysis extends CalledMethodsAnalysis { + /** + * Creates a new {@code CalledMethodsAnalysis}. + * + * @param checker the checker + * @param factory the factory + */ + protected ResourceLeakAnalysis( + BaseTypeChecker checker, CalledMethodsAnnotatedTypeFactory factory) { + super(checker, factory); + ignoredExceptionTypes.addAll(MustCallConsistencyAnalyzer.ignoredExceptionTypes); + } +} diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakAnnotatedTypeFactory.java index 500b23bce3..cd2ebcd021 100644 --- a/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakAnnotatedTypeFactory.java @@ -11,8 +11,6 @@ import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.Element; import javax.lang.model.element.ExecutableElement; -import javax.lang.model.element.TypeElement; -import javax.lang.model.type.TypeKind; import org.checkerframework.checker.calledmethods.CalledMethodsAnnotatedTypeFactory; import org.checkerframework.checker.calledmethods.qual.CalledMethods; import org.checkerframework.checker.calledmethods.qual.CalledMethodsBottom; @@ -26,20 +24,19 @@ import org.checkerframework.checker.mustcall.qual.MustCall; import org.checkerframework.checker.mustcall.qual.MustCallAlias; import org.checkerframework.checker.nullness.qual.Nullable; -import org.checkerframework.checker.resourceleak.MustCallConsistencyAnalyzer.ResourceAlias; import org.checkerframework.common.basetype.BaseTypeChecker; import org.checkerframework.dataflow.cfg.ControlFlowGraph; +import org.checkerframework.dataflow.cfg.UnderlyingAST; import org.checkerframework.dataflow.cfg.node.LocalVariableNode; import org.checkerframework.dataflow.cfg.node.MethodInvocationNode; import org.checkerframework.dataflow.cfg.node.Node; -import org.checkerframework.dataflow.expression.LocalVariable; import org.checkerframework.framework.flow.CFStore; -import org.checkerframework.framework.flow.CFValue; import org.checkerframework.framework.type.AnnotatedTypeMirror; import org.checkerframework.framework.type.GenericAnnotatedTypeFactory; import org.checkerframework.javacutil.AnnotationUtils; +import org.checkerframework.javacutil.ElementUtils; import org.checkerframework.javacutil.TreeUtils; -import org.checkerframework.javacutil.TypesUtils; +import org.checkerframework.javacutil.TypeSystemError; /** * The type factory for the Resource Leak Checker. The main difference between this and the Called @@ -86,6 +83,19 @@ public ResourceLeakAnnotatedTypeFactory(final BaseTypeChecker checker) { this.postInit(); } + /** + * Is the given element a candidate to be an owning field? A candidate owning field must be final + * and have a non-empty must-call obligation. + * + * @param element a element + * @return true iff the given element is a final field with non-empty @MustCall obligation + */ + boolean isCandidateOwningField(Element element) { + return (element.getKind().isField() + && ElementUtils.isFinal(element) + && !getMustCallValue(element).isEmpty()); + } + @Override protected Set> createSupportedTypeQualifiers() { return getBundledTypeQualifiers( @@ -107,67 +117,22 @@ public void postAnalyze(ControlFlowGraph cfg) { MustCallConsistencyAnalyzer mustCallConsistencyAnalyzer = new MustCallConsistencyAnalyzer(this, this.analysis); mustCallConsistencyAnalyzer.analyze(cfg); + + // Inferring owning annotations for final owning fields + if (getWholeProgramInference() != null) { + if (cfg.getUnderlyingAST().getKind() == UnderlyingAST.Kind.METHOD) { + MustCallInferenceLogic mustCallInferenceLogic = new MustCallInferenceLogic(this, cfg); + mustCallInferenceLogic.runInference(); + } + } + super.postAnalyze(cfg); tempVarToTree.clear(); } - /** - * Use the must-call store to get the must-call value of the resource represented by the given - * resource aliases. - * - * @param resourceAliasSet a set of resource aliases of the same resource - * @param mcStore a CFStore produced by the MustCall checker's dataflow analysis. If this is null, - * then the default MustCall type of each variable's class will be used. - * @return the list of must-call method names, or null if the resource's must-call obligations are - * unsatisfiable (i.e. its value in the Must Call store is MustCallUnknown) - */ - public @Nullable List getMustCallValue( - Set resourceAliasSet, @Nullable CFStore mcStore) { - MustCallAnnotatedTypeFactory mustCallAnnotatedTypeFactory = - getTypeFactoryOfSubchecker(MustCallChecker.class); - - // Need to get the LUB of the MC values, because if a CreatesMustCallFor method was - // called on just one of the locals then they all need to be treated as if - // they need to call the relevant methods. - AnnotationMirror mcLub = mustCallAnnotatedTypeFactory.BOTTOM; - for (ResourceAlias alias : resourceAliasSet) { - AnnotationMirror mcAnno = null; - LocalVariable reference = alias.reference; - CFValue value = mcStore == null ? null : mcStore.getValue(reference); - if (value != null) { - mcAnno = getAnnotationByClass(value.getAnnotations(), MustCall.class); - } - if (mcAnno == null) { - // It wasn't in the store, so fall back to the default must-call type for the class. - // TODO: we currently end up in this case when checking a call to the return type - // of a returns-receiver method on something with a MustCall type; for example, - // see tests/socket/ZookeeperReport6.java. We should instead use a poly type if we - // can. - TypeElement typeElt = TypesUtils.getTypeElement(reference.getType()); - if (typeElt == null) { - // typeElt is null if reference.getType() was not a class, interface, annotation type, or - // enum---that is, was not an annotatable type. - // That shouldn't happen, but if it does fall back to a safe default (i.e. top). - mcAnno = mustCallAnnotatedTypeFactory.TOP; - } else { - // TODO: Why does this happen sometimes? - if (typeElt.asType().getKind() == TypeKind.VOID) { - return Collections.emptyList(); - } - mcAnno = - mustCallAnnotatedTypeFactory - .getAnnotatedType(typeElt) - .getAnnotationInHierarchy(mustCallAnnotatedTypeFactory.TOP); - } - } - mcLub = mustCallAnnotatedTypeFactory.getQualifierHierarchy().leastUpperBound(mcLub, mcAnno); - } - if (AnnotationUtils.areSameByName( - mcLub, "org.checkerframework.checker.mustcall.qual.MustCall")) { - return getMustCallValues(mcLub); - } else { - return null; - } + @Override + protected ResourceLeakAnalysis createFlowAnalysis() { + return new ResourceLeakAnalysis(checker, this); } /** @@ -192,8 +157,11 @@ public void postAnalyze(ControlFlowGraph cfg) { * Returns the {@link MustCall#value} element/argument of the @MustCall annotation on the class * type of {@code element}. * - *

If possible, prefer {@link #getMustCallValue(Tree)}, which accounts for flow-sensitive - * refinement. + *

Do not use this method to get the MustCall value of an {@link + * org.checkerframework.checker.resourceleak.MustCallConsistencyAnalyzer.Obligation}. Instead, use + * {@link + * org.checkerframework.checker.resourceleak.MustCallConsistencyAnalyzer.Obligation#getMustCallMethods(ResourceLeakAnnotatedTypeFactory, + * CFStore)}. * * @param element an element * @return the strings in its must-call type @@ -214,7 +182,8 @@ public void postAnalyze(ControlFlowGraph cfg) { * @return the strings in mustCallAnnotation's value element, or the empty list if * mustCallAnnotation is null */ - private List getMustCallValues(@Nullable AnnotationMirror mustCallAnnotation) { + /* package-private */ List getMustCallValues( + @Nullable AnnotationMirror mustCallAnnotation) { if (mustCallAnnotation == null) { return Collections.emptyList(); } @@ -243,6 +212,18 @@ private List getMustCallValues(@Nullable AnnotationMirror mustCallAnnota return tempVarToTree.containsKey(node); } + /** + * Gets the tree for a temporary variable + * + * @param node a node for a temporary variable + * @return the tree for {@code node} + */ + /* package-private */ Tree getTreeForTempVar(Node node) { + if (!tempVarToTree.containsKey(node)) { + throw new TypeSystemError(node + " must be a temporary variable"); + } + return tempVarToTree.get(node); + } /** * Registers a temporary variable by adding it to this type factory's tempvar map. * @@ -256,13 +237,19 @@ private List getMustCallValues(@Nullable AnnotationMirror mustCallAnnota /** * Returns true if the type of the tree includes a must-call annotation. Note that this method may * not consider dataflow, and is only safe to use when you need the declared, rather than - * inferred, type of the tree. Use {@link #getMustCallValue(Set, CFStore)} (and check for - * emptiness) if you are trying to determine whether a local variable has must-call obligations. + * inferred, type of the tree. + * + *

Do not use this method if you are trying to get the must-call obligations of the resource + * aliases of an {@link + * org.checkerframework.checker.resourceleak.MustCallConsistencyAnalyzer.Obligation}. Instead, use + * {@link + * org.checkerframework.checker.resourceleak.MustCallConsistencyAnalyzer.Obligation#getMustCallMethods(ResourceLeakAnnotatedTypeFactory, + * CFStore)}. * * @param tree a tree * @return whether the tree has declared must-call obligations */ - /* package-private */ boolean hasDeclaredMustCall(Tree tree) { + /* package-private */ boolean declaredTypeHasMustCall(Tree tree) { assert tree.getKind() == Tree.Kind.METHOD || tree.getKind() == Tree.Kind.VARIABLE || tree.getKind() == Tree.Kind.NEW_CLASS @@ -347,9 +334,11 @@ public ExecutableElement getCreatesMustCallForValueElement() { } /** - * Returns the {@link CreatesMustCallFor.List#value} element. + * Returns the {@link org.checkerframework.checker.mustcall.qual.CreatesMustCallFor.List#value} + * element. * - * @return the {@link CreatesMustCallFor.List#value} element + * @return the {@link org.checkerframework.checker.mustcall.qual.CreatesMustCallFor.List#value} + * element */ @Override public ExecutableElement getCreatesMustCallForListValueElement() { diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakTransfer.java b/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakTransfer.java index dd8365243c..d1d10f57e9 100644 --- a/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakTransfer.java +++ b/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakTransfer.java @@ -14,7 +14,6 @@ import org.checkerframework.dataflow.cfg.node.ObjectCreationNode; import org.checkerframework.dataflow.cfg.node.TernaryExpressionNode; import org.checkerframework.dataflow.expression.JavaExpression; -import org.checkerframework.framework.flow.CFAnalysis; import org.checkerframework.framework.flow.CFStore; import org.checkerframework.framework.flow.CFValue; import org.checkerframework.javacutil.TypesUtils; @@ -34,7 +33,7 @@ public class ResourceLeakTransfer extends CalledMethodsTransfer { * @param analysis the analysis. Its type factory must be a {@link * ResourceLeakAnnotatedTypeFactory}. */ - public ResourceLeakTransfer(final CFAnalysis analysis) { + public ResourceLeakTransfer(final ResourceLeakAnalysis analysis) { super(analysis); this.rlTypeFactory = (ResourceLeakAnnotatedTypeFactory) analysis.getTypeFactory(); } @@ -43,7 +42,11 @@ public ResourceLeakTransfer(final CFAnalysis analysis) { public TransferResult visitTernaryExpression( TernaryExpressionNode node, TransferInput input) { TransferResult result = super.visitTernaryExpression(node, input); - updateStoreWithTempVar(result, node); + if (!TypesUtils.isPrimitiveOrBoxed(node.getType())) { + // Add the synthetic variable created during CFG construction to the temporary + // variable map (rather than creating a redundant temp var) + rlTypeFactory.addTempVar(node.getTernaryExpressionVar(), node.getTree()); + } return result; } diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakVisitor.java b/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakVisitor.java index ea820480e5..3b8cc3c905 100644 --- a/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakVisitor.java @@ -4,10 +4,12 @@ import com.sun.source.tree.VariableTree; import java.util.ArrayList; import java.util.List; +import java.util.Set; import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.Element; import javax.lang.model.element.ElementKind; import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.TypeElement; import org.checkerframework.checker.calledmethods.CalledMethodsVisitor; import org.checkerframework.checker.calledmethods.qual.EnsuresCalledMethods; import org.checkerframework.checker.mustcall.CreatesMustCallForElementSupplier; @@ -16,6 +18,10 @@ import org.checkerframework.checker.mustcall.qual.CreatesMustCallFor; import org.checkerframework.checker.mustcall.qual.Owning; import org.checkerframework.common.basetype.BaseTypeChecker; +import org.checkerframework.dataflow.expression.JavaExpression; +import org.checkerframework.framework.flow.CFAbstractStore; +import org.checkerframework.framework.flow.CFAbstractValue; +import org.checkerframework.framework.type.QualifierHierarchy; import org.checkerframework.javacutil.AnnotationUtils; import org.checkerframework.javacutil.ElementUtils; import org.checkerframework.javacutil.TreeUtils; @@ -80,6 +86,76 @@ public Void visitMethod(MethodTree node, Void p) { return super.visitMethod(node, p); } + // Overwritten to check that destructors (i.e. methods responsible for resolving + // the must-call obligations of owning fields) enforce a stronger version of + // @EnsuresCalledMethods: that the claimed @CalledMethods annotation is true on + // both exceptional and regular exits, not just on regular exits. + @Override + protected void checkPostcondition( + MethodTree methodTree, AnnotationMirror annotation, JavaExpression expression) { + super.checkPostcondition(methodTree, annotation, expression); + // Only check if the required annotation is a CalledMethods annotation (implying + // the method was annotated with @EnsuresCalledMethods). + if (!AnnotationUtils.areSameByName( + annotation, "org.checkerframework.checker.calledmethods.qual.CalledMethods")) { + return; + } + if (!isMustCallMethod(methodTree)) { + // In this case, the method has an EnsuresCalledMethods annotation but is not a destructor, + // so no further checking is required. + return; + } + CFAbstractStore exitStore = atypeFactory.getExceptionalExitStore(methodTree); + if (exitStore == null) { + // If there is no exceptional exitStore, then the method cannot throw an exception and there + // is no need to check anything else. + } else { + CFAbstractValue value = exitStore.getValue(expression); + AnnotationMirror inferredAnno = null; + if (value != null) { + QualifierHierarchy hierarchy = atypeFactory.getQualifierHierarchy(); + Set annos = value.getAnnotations(); + inferredAnno = hierarchy.findAnnotationInSameHierarchy(annos, annotation); + } + if (!checkContract(expression, annotation, inferredAnno, exitStore)) { + String inferredAnnoStr = + inferredAnno == null + ? "no information about " + expression.toString() + : inferredAnno.toString(); + checker.reportError( + methodTree, + "destructor.exceptional.postcondition", + methodTree.getName(), + expression.toString(), + inferredAnnoStr, + annotation); + } + } + } + + /** + * Returns true iff the {@code MustCall} annotation of the class that encloses the methodTree + * names this method. + * + * @param methodTree the declaration of a method + * @return whether that method is one of the must-call methods for its enclosing class + */ + private boolean isMustCallMethod(MethodTree methodTree) { + ExecutableElement elt = TreeUtils.elementFromDeclaration(methodTree); + TypeElement containingClass = ElementUtils.enclosingTypeElement(elt); + MustCallAnnotatedTypeFactory mustCallAnnotatedTypeFactory = + rlTypeFactory.getTypeFactoryOfSubchecker(MustCallChecker.class); + AnnotationMirror mcAnno = + mustCallAnnotatedTypeFactory + .getAnnotatedType(containingClass) + .getAnnotationInHierarchy(mustCallAnnotatedTypeFactory.TOP); + List mcValues = + AnnotationUtils.getElementValueArray( + mcAnno, mustCallAnnotatedTypeFactory.getMustCallValueElement(), String.class); + String methodName = elt.getSimpleName().toString(); + return mcValues.contains(methodName); + } + /** * Returns the {@link CreatesMustCallFor#value} element/argument of the given @CreatesMustCallFor * annotation, or "this" if there is none. @@ -228,6 +304,7 @@ private void checkOwningField(Element field) { "required.method.not.called", MustCallConsistencyAnalyzer.formatMissingMustCallMethods( unsatisfiedMustCallObligationsOfOwningField), + "field " + field.getSimpleName().toString(), field.asType().toString(), error); } diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/messages.properties b/checker/src/main/java/org/checkerframework/checker/resourceleak/messages.properties index b9bd04b4af..98adba524a 100644 --- a/checker/src/main/java/org/checkerframework/checker/resourceleak/messages.properties +++ b/checker/src/main/java/org/checkerframework/checker/resourceleak/messages.properties @@ -1,5 +1,7 @@ -required.method.not.called=@MustCall %s not invoked. The type of object is: %s. Reason for going out of scope: %s -missing.creates.mustcall.for=This method re-assigns the non-final, owning field %s.%s, but does not have a corresponding @CreatesMustCallFor annotation. -incompatible.creates.mustcall.for=This method re-assigns the non-final, owning field %s.%s, but its @CreatesMustCallFor annotation targets %s. -reset.not.owning=Calling this method resets the must-call obligations of the expression %s, which is non-owning. Either annotate its declaration with an @Owning annotation, extract it into a local variable, or write a corresponding @CreatesMustCallFor annotation on the method that encloses this statement. +required.method.not.called=@MustCall %s may not have been invoked on %s or any of its aliases.\nThe type of object is: %s.\nReason for going out of scope: %s +missing.creates.mustcall.for=Method %s re-assigns the non-final, owning field %s.%s, but does not have a corresponding @CreatesMustCallFor annotation. +incompatible.creates.mustcall.for=Method %s re-assigns the non-final, owning field %s.%s, but its @CreatesMustCallFor annotation targets %s. +reset.not.owning=Calling method %s resets the must-call obligations of the expression %s, which is non-owning. Either annotate its declaration with an @Owning annotation, extract it into a local variable, or write a corresponding @CreatesMustCallFor annotation on the method that encloses this statement. creates.mustcall.for.override.invalid=Method %s cannot override method %s, which defines fewer @CreatesMustCallFor targets.\nfound: %s\nrequired: %s +destructor.exceptional.postcondition=Method %s must resolve the must-call obligations of the owning field %s along all paths, including exceptional paths. On an exceptional path, the @EnsuresCalledMethods annotation was not satisfied.\nFound: %s\nRequired: %s +mustcallalias.out.of.scope=This @MustCallAlias parameter might go out of scope without being assigned into an owning field of this object (if this is a constructor) or returned.\nReason for going out of scope: %s diff --git a/checker/src/main/java/org/checkerframework/checker/signedness/SignednessAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/signedness/SignednessAnnotatedTypeFactory.java index ab456030c1..2b5a568751 100644 --- a/checker/src/main/java/org/checkerframework/checker/signedness/SignednessAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/signedness/SignednessAnnotatedTypeFactory.java @@ -39,11 +39,11 @@ import org.checkerframework.framework.type.treeannotator.TreeAnnotator; import org.checkerframework.javacutil.AnnotationBuilder; import org.checkerframework.javacutil.AnnotationUtils; -import org.checkerframework.javacutil.BugInCF; import org.checkerframework.javacutil.Pair; import org.checkerframework.javacutil.TreePathUtil; import org.checkerframework.javacutil.TreeUtils; import org.checkerframework.javacutil.TypeKindUtils; +import org.checkerframework.javacutil.TypeSystemError; import org.checkerframework.javacutil.TypesUtils; /** @@ -420,7 +420,7 @@ private boolean maskIgnoresMSB( // Check that the shiftAmount most significant bits of the mask were 1. return mask == (1 << shiftAmount) - 1; } else { - throw new BugInCF("Invalid Masking Operation"); + throw new TypeSystemError("Invalid Masking Operation"); } } @@ -466,7 +466,7 @@ private boolean castIgnoresMSB( shiftAmount = 0x3F & getLong(shiftAmountLit.getValue()); break; default: - throw new BugInCF("Invalid shift type"); + throw new TypeSystemError("Invalid shift type"); } // Determine number of bits in the cast type @@ -488,7 +488,7 @@ private boolean castIgnoresMSB( castBits = 64; break; default: - throw new BugInCF("Invalid cast target"); + throw new TypeSystemError("Invalid cast target"); } long bitsDiscarded = shiftBits - castBits; diff --git a/checker/src/main/java/org/checkerframework/checker/units/UnitsAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/units/UnitsAnnotatedTypeFactory.java index 64cf8eb525..fef9ecf00d 100644 --- a/checker/src/main/java/org/checkerframework/checker/units/UnitsAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/units/UnitsAnnotatedTypeFactory.java @@ -383,7 +383,7 @@ private void addUnitsRelations(Class qual) { classname, unitsRelationsClass.getDeclaredConstructor().newInstance().init(processingEnv)); } catch (Throwable e) { - throw new BugInCF("Throwable when instantiating UnitsRelations", e); + throw new TypeSystemError("Throwable when instantiating UnitsRelations", e); } } } @@ -604,10 +604,14 @@ protected AnnotationMirror leastUpperBoundWithElements( return lub; } } - throw new BugInCF("Unexpected QualifierKinds: %s %s", qualifierKind1, qualifierKind2); + throw new TypeSystemError("Unexpected QualifierKinds: %s %s", qualifierKind1, qualifierKind2); } @Override + @SuppressWarnings( + "nullness:return" // This class UnitsQualifierHierarchy is annotated for nullness, but the + // outer class UnitsAnnotatedTypeFactory is not, so the type of fields is @Nullable. + ) protected AnnotationMirror greatestLowerBoundWithElements( AnnotationMirror a1, QualifierKind qualifierKind1, @@ -685,7 +689,7 @@ private QualifierKind getDirectSuperQualifierKind( } superQuals.removeAll(lowest); } - throw new BugInCF("No direct super qualifier found for %s", qualifierKind); + throw new TypeSystemError("No direct super qualifier found for %s", qualifierKind); } } diff --git a/checker/src/test/java/org/checkerframework/checker/test/junit/CalledMethodsNoDelombokTest.java b/checker/src/test/java/org/checkerframework/checker/test/junit/CalledMethodsNoDelombokTest.java new file mode 100644 index 0000000000..f53ee643d3 --- /dev/null +++ b/checker/src/test/java/org/checkerframework/checker/test/junit/CalledMethodsNoDelombokTest.java @@ -0,0 +1,61 @@ +package org.checkerframework.checker.test.junit; + +import com.google.common.collect.ImmutableList; +import java.io.File; +import java.util.Collections; +import java.util.List; +import org.checkerframework.framework.test.CheckerFrameworkPerDirectoryTest; +import org.checkerframework.framework.test.TestConfiguration; +import org.checkerframework.framework.test.TestConfigurationBuilder; +import org.checkerframework.framework.test.TestUtilities; +import org.checkerframework.framework.test.TypecheckExecutor; +import org.checkerframework.framework.test.TypecheckResult; +import org.junit.runners.Parameterized.Parameters; + +/** + * This test suite exists to demonstrate and keep a record of the unsoundness that occurs when + * Lombok and the Checker Framework are run in the same invocation of javac. + */ +public class CalledMethodsNoDelombokTest extends CheckerFrameworkPerDirectoryTest { + + private static final ImmutableList ANNOTATION_PROCS = + ImmutableList.of( + "lombok.launch.AnnotationProcessorHider$AnnotationProcessor", + "lombok.launch.AnnotationProcessorHider$ClaimingProcessor", + org.checkerframework.checker.calledmethods.CalledMethodsChecker.class.getName()); + + public CalledMethodsNoDelombokTest(List testFiles) { + super( + testFiles, + org.checkerframework.checker.calledmethods.CalledMethodsChecker.class, + "lombok", + "-Anomsgtext", + "-nowarn"); + } + + @Parameters + public static String[] getTestDirs() { + return new String[] {"calledmethods-nodelombok"}; + } + + /** + * copy-pasted code from {@link CheckerFrameworkPerDirectoryTest#run()}, except that we change the + * annotation processors to {@link #ANNOTATION_PROCS} + */ + @Override + public void run() { + boolean shouldEmitDebugInfo = TestUtilities.getShouldEmitDebugInfo(); + List customizedOptions = customizeOptions(Collections.unmodifiableList(checkerOptions)); + TestConfiguration config = + TestConfigurationBuilder.buildDefaultConfiguration( + testDir, + testFiles, + classpathExtra, + ANNOTATION_PROCS, + customizedOptions, + shouldEmitDebugInfo); + TypecheckResult testResult = new TypecheckExecutor().runTest(config); + TypecheckResult adjustedTestResult = adjustTypecheckResult(testResult); + TestUtilities.assertTestDidNotFail(adjustedTestResult); + } +} diff --git a/checker/src/test/java/org/checkerframework/checker/test/junit/DisbarUseTest.java b/checker/src/test/java/org/checkerframework/checker/test/junit/DisbarUseTest.java new file mode 100644 index 0000000000..f804e80ae8 --- /dev/null +++ b/checker/src/test/java/org/checkerframework/checker/test/junit/DisbarUseTest.java @@ -0,0 +1,33 @@ +package org.checkerframework.checker.test.junit; + +import java.io.File; +import java.util.List; +import org.checkerframework.checker.testchecker.disbaruse.DisbarUseChecker; +import org.checkerframework.framework.test.CheckerFrameworkPerDirectoryTest; +import org.junit.runners.Parameterized.Parameters; + +public class DisbarUseTest extends CheckerFrameworkPerDirectoryTest { + + /** + * Create a DisbarUseTest. + * + * @param testFiles the files containing test code, which will be type-checked + */ + public DisbarUseTest(List testFiles) { + super( + testFiles, + DisbarUseChecker.class, + "disbaruse-records", + "-Anomsgtext", + "-Astubs=tests/disbaruse-records", + "-AstubWarnIfNotFound"); + } + + @Parameters + public static String[] getTestDirs() { + // Check for JDK 16+ without using a library: + if (System.getProperty("java.version").matches("^(1[6-9]|[2-9][0-9])\\..*")) + return new String[] {"disbaruse-records"}; + else return new String[] {}; + } +} diff --git a/checker/src/test/java/org/checkerframework/checker/test/junit/LockTest.java b/checker/src/test/java/org/checkerframework/checker/test/junit/LockTest.java index 251c01325f..6ec8ce1a5f 100644 --- a/checker/src/test/java/org/checkerframework/checker/test/junit/LockTest.java +++ b/checker/src/test/java/org/checkerframework/checker/test/junit/LockTest.java @@ -18,6 +18,9 @@ public LockTest(List testFiles) { @Parameters public static String[] getTestDirs() { - return new String[] {"lock", "all-systems"}; + // Check for JDK 16+ without using a library: + if (System.getProperty("java.version").matches("^(1[6-9]|[2-9][0-9])\\..*")) + return new String[] {"lock", "lock-records", "all-systems"}; + else return new String[] {"lock", "all-systems"}; } } diff --git a/checker/src/test/java/org/checkerframework/checker/test/junit/NullnessNoDelombokTest.java b/checker/src/test/java/org/checkerframework/checker/test/junit/NullnessNoDelombokTest.java new file mode 100644 index 0000000000..d37430443b --- /dev/null +++ b/checker/src/test/java/org/checkerframework/checker/test/junit/NullnessNoDelombokTest.java @@ -0,0 +1,61 @@ +package org.checkerframework.checker.test.junit; + +import com.google.common.collect.ImmutableList; +import java.io.File; +import java.util.Collections; +import java.util.List; +import org.checkerframework.framework.test.CheckerFrameworkPerDirectoryTest; +import org.checkerframework.framework.test.TestConfiguration; +import org.checkerframework.framework.test.TestConfigurationBuilder; +import org.checkerframework.framework.test.TestUtilities; +import org.checkerframework.framework.test.TypecheckExecutor; +import org.checkerframework.framework.test.TypecheckResult; +import org.junit.runners.Parameterized.Parameters; + +/** + * This test suite exists to demonstrate and keep a record of the unsoundness that occurs when + * Lombok and the Checker Framework are run in the same invocation of javac. + */ +public class NullnessNoDelombokTest extends CheckerFrameworkPerDirectoryTest { + + private static final ImmutableList ANNOTATION_PROCS = + ImmutableList.of( + "lombok.launch.AnnotationProcessorHider$AnnotationProcessor", + "lombok.launch.AnnotationProcessorHider$ClaimingProcessor", + org.checkerframework.checker.nullness.NullnessChecker.class.getName()); + + public NullnessNoDelombokTest(List testFiles) { + super( + testFiles, + org.checkerframework.checker.nullness.NullnessChecker.class, + "nullness-nodelombok", + "-Anomsgtext", + "-nowarn"); + } + + @Parameters + public static String[] getTestDirs() { + return new String[] {"nullness-nodelombok"}; + } + + /** + * copy-pasted code from {@link CheckerFrameworkPerDirectoryTest#run()}, except that we change the + * annotation processors to {@link #ANNOTATION_PROCS} + */ + @Override + public void run() { + boolean shouldEmitDebugInfo = TestUtilities.getShouldEmitDebugInfo(); + List customizedOptions = customizeOptions(Collections.unmodifiableList(checkerOptions)); + TestConfiguration config = + TestConfigurationBuilder.buildDefaultConfiguration( + testDir, + testFiles, + classpathExtra, + ANNOTATION_PROCS, + customizedOptions, + shouldEmitDebugInfo); + TypecheckResult testResult = new TypecheckExecutor().runTest(config); + TypecheckResult adjustedTestResult = adjustTypecheckResult(testResult); + TestUtilities.assertTestDidNotFail(adjustedTestResult); + } +} diff --git a/checker/src/test/java/org/checkerframework/checker/test/junit/NullnessRecordsTest.java b/checker/src/test/java/org/checkerframework/checker/test/junit/NullnessRecordsTest.java new file mode 100644 index 0000000000..fafb8ddccc --- /dev/null +++ b/checker/src/test/java/org/checkerframework/checker/test/junit/NullnessRecordsTest.java @@ -0,0 +1,35 @@ +package org.checkerframework.checker.test.junit; + +import java.io.File; +import java.util.List; +import org.checkerframework.checker.nullness.NullnessChecker; +import org.checkerframework.framework.test.CheckerFrameworkPerDirectoryTest; +import org.junit.runners.Parameterized.Parameters; + +/** JUnit tests for the Nullness checker with records (JDK16+ only). */ +public class NullnessRecordsTest extends CheckerFrameworkPerDirectoryTest { + + /** + * Create a NullnessRecordsTest. + * + * @param testFiles the files containing test code, which will be type-checked + */ + public NullnessRecordsTest(List testFiles) { + super( + testFiles, + NullnessChecker.class, + "nullness-records", + "-AcheckPurityAnnotations", + "-Anomsgtext", + "-Xlint:deprecation"); + } + + @Parameters + public static String[] getTestDirs() { + // Check for JDK 16+ without using a library: + // There is no decimal point in the JDK 17 version number. + if (System.getProperty("java.version").matches("^(1[6-9]|[2-9][0-9])")) + return new String[] {"nullness-records"}; + else return new String[] {}; + } +} diff --git a/checker/src/test/java/org/checkerframework/checker/test/junit/ResourceLeakNoCreatesMustCallForTest.java b/checker/src/test/java/org/checkerframework/checker/test/junit/ResourceLeakNoCreatesMustCallForTest.java index b2c9437700..d4081f190b 100644 --- a/checker/src/test/java/org/checkerframework/checker/test/junit/ResourceLeakNoCreatesMustCallForTest.java +++ b/checker/src/test/java/org/checkerframework/checker/test/junit/ResourceLeakNoCreatesMustCallForTest.java @@ -15,7 +15,7 @@ public ResourceLeakNoCreatesMustCallForTest(List testFiles) { "resourceleak-nocreatesmustcallfor", "-Anomsgtext", "-AnoCreatesMustCallFor", - "-nowarn", + "-AwarnUnneededSuppressions", "-encoding", "UTF-8"); } diff --git a/checker/src/test/java/org/checkerframework/checker/test/junit/ResourceLeakTest.java b/checker/src/test/java/org/checkerframework/checker/test/junit/ResourceLeakTest.java index 5b7fd2a170..431dd97d8b 100644 --- a/checker/src/test/java/org/checkerframework/checker/test/junit/ResourceLeakTest.java +++ b/checker/src/test/java/org/checkerframework/checker/test/junit/ResourceLeakTest.java @@ -14,7 +14,7 @@ public ResourceLeakTest(List testFiles) { ResourceLeakChecker.class, "resourceleak", "-Anomsgtext", - "-nowarn", + "-AwarnUnneededSuppressions", "-encoding", "UTF-8"); } diff --git a/checker/src/test/java/org/checkerframework/checker/test/junit/StubparserRecordTest.java b/checker/src/test/java/org/checkerframework/checker/test/junit/StubparserRecordTest.java new file mode 100644 index 0000000000..f1de05f25e --- /dev/null +++ b/checker/src/test/java/org/checkerframework/checker/test/junit/StubparserRecordTest.java @@ -0,0 +1,34 @@ +package org.checkerframework.checker.test.junit; + +import java.io.File; +import java.util.List; +import org.checkerframework.framework.test.CheckerFrameworkPerDirectoryTest; +import org.junit.runners.Parameterized; + +/** Tests for stub parsing with records. */ +public class StubparserRecordTest extends CheckerFrameworkPerDirectoryTest { + + /** + * Create a StubparserRecordTest. + * + * @param testFiles the files containing test code, which will be type-checked + */ + public StubparserRecordTest(List testFiles) { + super( + testFiles, + org.checkerframework.checker.nullness.NullnessChecker.class, + "stubparser-records", + "-Anomsgtext", + "-Astubs=tests/stubparser-records", + "-AstubWarnIfNotFound"); + } + + @Parameterized.Parameters + public static String[] getTestDirs() { + // Check for JDK 16+ without using a library: + // There is no decimal point in the JDK 17 version number. + if (System.getProperty("java.version").matches("^(1[6-9]|[2-9][0-9])")) + return new String[] {"stubparser-records"}; + else return new String[] {}; + } +} diff --git a/checker/src/test/java/org/checkerframework/checker/testchecker/ainfer/AinferTestAnnotatedTypeFactory.java b/checker/src/test/java/org/checkerframework/checker/testchecker/ainfer/AinferTestAnnotatedTypeFactory.java index 6fb6b0aad8..bfc0429a25 100644 --- a/checker/src/test/java/org/checkerframework/checker/testchecker/ainfer/AinferTestAnnotatedTypeFactory.java +++ b/checker/src/test/java/org/checkerframework/checker/testchecker/ainfer/AinferTestAnnotatedTypeFactory.java @@ -30,8 +30,8 @@ import org.checkerframework.framework.util.QualifierKind; import org.checkerframework.javacutil.AnnotationBuilder; import org.checkerframework.javacutil.AnnotationUtils; -import org.checkerframework.javacutil.BugInCF; import org.checkerframework.javacutil.TreeUtils; +import org.checkerframework.javacutil.TypeSystemError; /** * AnnotatedTypeFactory to test whole-program inference using .jaif files. @@ -135,7 +135,7 @@ protected AnnotationMirror greatestLowerBoundWithElements( } else if (qualifierKind2 == SIBLING_WITH_FIELDS_KIND) { return a2; } - throw new BugInCF("Unexpected qualifiers: %s %s", a1, a2); + throw new TypeSystemError("Unexpected qualifiers: %s %s", a1, a2); } @Override @@ -156,7 +156,7 @@ protected AnnotationMirror leastUpperBoundWithElements( } else if (qualifierKind2 == SIBLING_WITH_FIELDS_KIND) { return a2; } - throw new BugInCF("Unexpected qualifiers: %s %s", a1, a2); + throw new TypeSystemError("Unexpected qualifiers: %s %s", a1, a2); } @Override @@ -180,7 +180,7 @@ protected boolean isSubtypeWithElements( superAnno, siblingWithFieldsValue2Element, String.class, ""); return subVal1.equals(supVal1) && subVal2.equals(supVal2); } - throw new BugInCF("Unexpected qualifiers: %s %s", subAnno, superAnno); + throw new TypeSystemError("Unexpected qualifiers: %s %s", subAnno, superAnno); } } } diff --git a/checker/src/test/java/org/checkerframework/checker/testchecker/disbaruse/DisbarUseChecker.java b/checker/src/test/java/org/checkerframework/checker/testchecker/disbaruse/DisbarUseChecker.java new file mode 100644 index 0000000000..6d603c84e0 --- /dev/null +++ b/checker/src/test/java/org/checkerframework/checker/testchecker/disbaruse/DisbarUseChecker.java @@ -0,0 +1,9 @@ +package org.checkerframework.checker.testchecker.disbaruse; + +import org.checkerframework.common.basetype.BaseTypeChecker; + +/** + * A checker that issues a "disbar.use" error at any use of fields, methods or parameters whose type + * is {@code @DisbarUse}. + */ +public class DisbarUseChecker extends BaseTypeChecker {} diff --git a/checker/src/test/java/org/checkerframework/checker/testchecker/disbaruse/DisbarUseTypeFactory.java b/checker/src/test/java/org/checkerframework/checker/testchecker/disbaruse/DisbarUseTypeFactory.java new file mode 100644 index 0000000000..c3812c3eba --- /dev/null +++ b/checker/src/test/java/org/checkerframework/checker/testchecker/disbaruse/DisbarUseTypeFactory.java @@ -0,0 +1,22 @@ +package org.checkerframework.checker.testchecker.disbaruse; + +import java.lang.annotation.Annotation; +import java.util.Arrays; +import java.util.LinkedHashSet; +import java.util.Set; +import org.checkerframework.checker.testchecker.disbaruse.qual.DisbarUseBottom; +import org.checkerframework.checker.testchecker.disbaruse.qual.DisbarUseTop; +import org.checkerframework.common.basetype.BaseAnnotatedTypeFactory; +import org.checkerframework.common.basetype.BaseTypeChecker; + +public class DisbarUseTypeFactory extends BaseAnnotatedTypeFactory { + public DisbarUseTypeFactory(BaseTypeChecker checker) { + super(checker); + postInit(); + } + + @Override + protected Set> createSupportedTypeQualifiers() { + return new LinkedHashSet<>(Arrays.asList(DisbarUseTop.class, DisbarUseBottom.class)); + } +} diff --git a/checker/src/test/java/org/checkerframework/checker/testchecker/disbaruse/DisbarUseVisitor.java b/checker/src/test/java/org/checkerframework/checker/testchecker/disbaruse/DisbarUseVisitor.java new file mode 100644 index 0000000000..bae45d7842 --- /dev/null +++ b/checker/src/test/java/org/checkerframework/checker/testchecker/disbaruse/DisbarUseVisitor.java @@ -0,0 +1,69 @@ +package org.checkerframework.checker.testchecker.disbaruse; + +import com.sun.source.tree.ExpressionTree; +import com.sun.source.tree.IdentifierTree; +import com.sun.source.tree.MemberSelectTree; +import com.sun.source.tree.MethodInvocationTree; +import com.sun.source.tree.NewClassTree; +import javax.lang.model.element.Element; +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.ExecutableElement; +import org.checkerframework.checker.testchecker.disbaruse.qual.DisbarUse; +import org.checkerframework.common.basetype.BaseTypeChecker; +import org.checkerframework.common.basetype.BaseTypeVisitor; +import org.checkerframework.javacutil.TreeUtils; + +public class DisbarUseVisitor extends BaseTypeVisitor { + public DisbarUseVisitor(BaseTypeChecker checker) { + super(checker); + } + + protected DisbarUseVisitor(BaseTypeChecker checker, DisbarUseTypeFactory typeFactory) { + super(checker, typeFactory); + } + + @Override + protected DisbarUseTypeFactory createTypeFactory() { + return new DisbarUseTypeFactory(checker); + } + + @Override + public Void visitMethodInvocation(MethodInvocationTree node, Void p) { + ExecutableElement methodElt = TreeUtils.elementFromUse(node); + if (methodElt != null && atypeFactory.getDeclAnnotation(methodElt, DisbarUse.class) != null) { + checker.reportError(node, "disbar.use"); + } + return super.visitMethodInvocation(node, p); + } + + @Override + public Void visitNewClass(NewClassTree node, Void p) { + ExecutableElement consElt = TreeUtils.elementFromUse(node); + if (consElt != null && atypeFactory.getDeclAnnotation(consElt, DisbarUse.class) != null) { + checker.reportError(node, "disbar.use"); + } + return super.visitNewClass(node, p); + } + + @Override + public Void visitIdentifier(IdentifierTree node, Void p) { + MemberSelectTree enclosingMemberSel = enclosingMemberSelect(); + ExpressionTree[] expressionTrees = + enclosingMemberSel == null + ? new ExpressionTree[] {node} + : new ExpressionTree[] {enclosingMemberSel, node}; + + for (ExpressionTree memberSel : expressionTrees) { + final Element elem = TreeUtils.elementFromUse(memberSel); + + // We only issue errors for variables that are fields or parameters: + if (elem != null && (elem.getKind().isField() || elem.getKind() == ElementKind.PARAMETER)) { + if (atypeFactory.getDeclAnnotation(elem, DisbarUse.class) != null) { + checker.reportError(memberSel, "disbar.use"); + } + } + } + + return super.visitIdentifier(node, p); + } +} diff --git a/checker/src/test/java/org/checkerframework/checker/testchecker/disbaruse/messages.properties b/checker/src/test/java/org/checkerframework/checker/testchecker/disbaruse/messages.properties new file mode 100644 index 0000000000..7cad93edb5 --- /dev/null +++ b/checker/src/test/java/org/checkerframework/checker/testchecker/disbaruse/messages.properties @@ -0,0 +1 @@ +disbar.use=Use of this element is disbarred diff --git a/checker/src/test/java/org/checkerframework/checker/testchecker/disbaruse/qual/DisbarUse.java b/checker/src/test/java/org/checkerframework/checker/testchecker/disbaruse/qual/DisbarUse.java new file mode 100644 index 0000000000..720e5cbf1b --- /dev/null +++ b/checker/src/test/java/org/checkerframework/checker/testchecker/disbaruse/qual/DisbarUse.java @@ -0,0 +1,8 @@ +package org.checkerframework.checker.testchecker.disbaruse.qual; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Target; + +/** The Disbar Use Checker isses a warning when an expression of this type is used. */ +@Target({ElementType.METHOD, ElementType.CONSTRUCTOR, ElementType.FIELD, ElementType.PARAMETER}) +public @interface DisbarUse {} diff --git a/checker/src/test/java/org/checkerframework/checker/testchecker/disbaruse/qual/DisbarUseBottom.java b/checker/src/test/java/org/checkerframework/checker/testchecker/disbaruse/qual/DisbarUseBottom.java new file mode 100644 index 0000000000..43d9fecf6b --- /dev/null +++ b/checker/src/test/java/org/checkerframework/checker/testchecker/disbaruse/qual/DisbarUseBottom.java @@ -0,0 +1,12 @@ +package org.checkerframework.checker.testchecker.disbaruse.qual; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Target; +import org.checkerframework.framework.qual.DefaultFor; +import org.checkerframework.framework.qual.SubtypeOf; +import org.checkerframework.framework.qual.TypeUseLocation; + +@DefaultFor(TypeUseLocation.LOWER_BOUND) +@SubtypeOf(DisbarUseTop.class) +@Target({ElementType.TYPE_USE}) +public @interface DisbarUseBottom {} diff --git a/checker/src/test/java/org/checkerframework/checker/testchecker/disbaruse/qual/DisbarUseTop.java b/checker/src/test/java/org/checkerframework/checker/testchecker/disbaruse/qual/DisbarUseTop.java new file mode 100644 index 0000000000..39749401b0 --- /dev/null +++ b/checker/src/test/java/org/checkerframework/checker/testchecker/disbaruse/qual/DisbarUseTop.java @@ -0,0 +1,11 @@ +package org.checkerframework.checker.testchecker.disbaruse.qual; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Target; +import org.checkerframework.framework.qual.DefaultQualifierInHierarchy; +import org.checkerframework.framework.qual.SubtypeOf; + +@Target({ElementType.TYPE_USE}) +@SubtypeOf({}) +@DefaultQualifierInHierarchy +public @interface DisbarUseTop {} diff --git a/checker/tests/ainfer-testchecker/non-annotated/EnsuresQualifierFieldDecl.java b/checker/tests/ainfer-testchecker/non-annotated/EnsuresQualifierFieldDecl.java new file mode 100644 index 0000000000..6d9df90ca7 --- /dev/null +++ b/checker/tests/ainfer-testchecker/non-annotated/EnsuresQualifierFieldDecl.java @@ -0,0 +1,14 @@ +// This test ensures that a field having a non-default inferred type +// does not cause inference to issue an @EnsuresQualifier annotation +// stating that fact on every method in the class, even those in which +// the field is not mentioned (and therefore not in the store, making +// them unverifiable). + +import org.checkerframework.checker.testchecker.ainfer.qual.Sibling1; + +class EnsuresQualifierFieldDecl { + @Sibling1 Object bar; + + // No annotation should be inferred here. + void test() {} +} diff --git a/checker/tests/ainfer-testchecker/non-annotated/EnsuresQualifierParamsTest.java b/checker/tests/ainfer-testchecker/non-annotated/EnsuresQualifierParamsTest.java new file mode 100644 index 0000000000..674ca4c1de --- /dev/null +++ b/checker/tests/ainfer-testchecker/non-annotated/EnsuresQualifierParamsTest.java @@ -0,0 +1,184 @@ +import org.checkerframework.checker.testchecker.ainfer.qual.AinferBottom; +import org.checkerframework.checker.testchecker.ainfer.qual.Parent; +import org.checkerframework.checker.testchecker.ainfer.qual.Sibling1; +import org.checkerframework.checker.testchecker.ainfer.qual.Sibling2; +import org.checkerframework.framework.qual.EnsuresQualifier; + +class EnsuresQualifierParamsTest { + + // these methods are used to infer types + + @SuppressWarnings("contracts.postcondition") // establish ground truth + @EnsuresQualifier(expression = "#1", qualifier = Parent.class) + void becomeParent(Object arg) {} + + @SuppressWarnings("contracts.postcondition") // establish ground truth + @EnsuresQualifier(expression = "#1", qualifier = Sibling1.class) + void becomeSibling1(Object arg) {} + + @SuppressWarnings("contracts.postcondition") // establish ground truth + @EnsuresQualifier(expression = "#1", qualifier = Sibling2.class) + void becomeSibling2(Object arg) {} + + @SuppressWarnings("contracts.postcondition") // establish ground truth + @EnsuresQualifier(expression = "#1", qualifier = AinferBottom.class) + void becomeBottom(Object arg) {} + + // these methods should have types inferred for them + + void argIsParent(Object arg) { + becomeParent(arg); + } + + void argIsParent_2(Object arg, boolean b) { + if (b) { + becomeSibling1(arg); + } else { + becomeSibling2(arg); + } + } + + void argIsSibling2(Object arg) { + becomeSibling2(arg); + } + + void argIsSibling2_2(Object arg, boolean b) { + if (b) { + becomeSibling2(arg); + } else { + becomeBottom(arg); + } + } + + void thisIsParent() { + becomeParent(this); + } + + void thisIsParent_2(boolean b) { + if (b) { + becomeSibling1(this); + } else { + becomeSibling2(this); + } + } + + void thisIsParent_2_2(boolean b) { + if (b) { + becomeSibling2(this); + } else { + becomeSibling1(this); + } + } + + void thisIsParent_3(boolean b) { + if (b) { + becomeSibling1(this); + } else { + becomeSibling2(this); + } + noEnsures(); + } + + void thisIsEmpty(boolean b) { + if (b) { + // do nothing + this.noEnsures(); + } else { + becomeSibling1(this); + } + } + + void thisIsSibling2() { + becomeSibling2(this); + } + + void thisIsSibling2_2(boolean b) { + if (b) { + becomeSibling2(this); + } else { + becomeBottom(this); + } + } + + void thisIsSibling2_2_2(boolean b) { + if (b) { + becomeBottom(this); + } else { + becomeSibling2(this); + } + } + + void noEnsures() {} + + void client1(Object arg) { + argIsParent(arg); + // :: warning: (assignment) + @Parent Object p = arg; + } + + void client2(Object arg) { + argIsParent_2(arg, true); + // :: warning: (assignment) + @Parent Object p = arg; + } + + void client3(Object arg) { + argIsSibling2(arg); + // :: warning: (assignment) + @Sibling2 Object x = arg; + } + + void client4(Object arg) { + argIsSibling2_2(arg, true); + // :: warning: (assignment) + @Sibling2 Object x = arg; + } + + void clientThis1() { + thisIsParent(); + // :: warning: (assignment) + @Parent Object o = this; + } + + void clientThis2() { + thisIsParent_2(true); + // :: warning: (assignment) + @Parent Object o = this; + } + + void clientThis2_2() { + thisIsParent_2(false); + // :: warning: (assignment) + @Parent Object o = this; + } + + void clientThis2_3() { + thisIsParent_3(false); + // :: warning: (assignment) + @Parent Object o = this; + } + + void clientThis3() { + thisIsSibling2(); + // :: warning: (assignment) + @Sibling2 Object o = this; + } + + void clientThis4() { + thisIsSibling2_2(true); + // :: warning: (assignment) + @Sibling2 Object o = this; + } + + void clientThis5() { + thisIsSibling2_2_2(true); + // :: warning: (assignment) + @Sibling2 Object o = this; + } + + void clientThis6() { + thisIsParent_2_2(true); + // :: warning: (assignment) + @Parent Object o = this; + } +} diff --git a/checker/tests/ainfer-testchecker/non-annotated/InterfaceTest.java b/checker/tests/ainfer-testchecker/non-annotated/InterfaceTest.java index 837787f694..8c7af17bf7 100644 --- a/checker/tests/ainfer-testchecker/non-annotated/InterfaceTest.java +++ b/checker/tests/ainfer-testchecker/non-annotated/InterfaceTest.java @@ -13,7 +13,7 @@ interface InterfaceTest { default void requireSibling1(@Sibling1 String x) {} default void testX() { - // :: warning: argument + // :: warning: (argument) requireSibling1(toaster); } } diff --git a/checker/tests/calledmethods-lombok/CheckerFrameworkBuilder.java b/checker/tests/calledmethods-lombok/CheckerFrameworkBuilder.java index 2dcfa6c144..17ec291659 100644 --- a/checker/tests/calledmethods-lombok/CheckerFrameworkBuilder.java +++ b/checker/tests/calledmethods-lombok/CheckerFrameworkBuilder.java @@ -118,7 +118,9 @@ public CheckerFrameworkBuilder.CheckerFrameworkBuilderBuilder z( @org.checkerframework.checker.builder.qual.ReturnsReceiver @java.lang.SuppressWarnings("all") public CheckerFrameworkBuilder.CheckerFrameworkBuilderBuilder name(final String name) { - if (this.names == null) this.names = new java.util.ArrayList(); + if (this.names == null) { + this.names = new java.util.ArrayList(); + } this.names.add(name); return this; } @@ -130,7 +132,9 @@ public CheckerFrameworkBuilder.CheckerFrameworkBuilderBuilder names( if (names == null) { throw new java.lang.NullPointerException("names cannot be null"); } - if (this.names == null) this.names = new java.util.ArrayList(); + if (this.names == null) { + this.names = new java.util.ArrayList(); + } this.names.addAll(names); return this; } @@ -138,7 +142,9 @@ public CheckerFrameworkBuilder.CheckerFrameworkBuilderBuilder names( @org.checkerframework.checker.builder.qual.ReturnsReceiver @java.lang.SuppressWarnings("all") public CheckerFrameworkBuilder.CheckerFrameworkBuilderBuilder clearNames() { - if (this.names != null) this.names.clear(); + if (this.names != null) { + this.names.clear(); + } return this; } @@ -161,7 +167,9 @@ public CheckerFrameworkBuilder build( java.util.Collections.unmodifiableList(new java.util.ArrayList(this.names)); } int x$value = this.x$value; - if (!this.x$set) x$value = CheckerFrameworkBuilder.$default$x(); + if (!this.x$set) { + x$value = CheckerFrameworkBuilder.$default$x(); + } return new CheckerFrameworkBuilder(x$value, this.y, this.z, names); } diff --git a/checker/tests/calledmethods-nodelombok/UnsoundnessTest.java b/checker/tests/calledmethods-nodelombok/UnsoundnessTest.java new file mode 100644 index 0000000000..f20b84a5f7 --- /dev/null +++ b/checker/tests/calledmethods-nodelombok/UnsoundnessTest.java @@ -0,0 +1,21 @@ +// An example of an unsoundness that occurs when running the Called Methods Checker +// on Lombok'd code without running delombok first. + +@lombok.Builder +class UnsoundnessTest { + @lombok.NonNull Object foo; + @lombok.NonNull Object bar; + + static void test() { + // An error should be issued here, but the code has not been delombok'd. + // If the CF and Lombok are ever able to work in the same invocation of javac + // (i.e. without delomboking first), then this error should be changed back to an + // expected error by re-adding the leading "::". + // error: (finalizer.invocation) + builder().build(); + } + + static void test2() { + builder().foo(null).bar(null).build(); + } +} diff --git a/checker/tests/calledmethods/EnsuresCalledMethodsIfTest.java b/checker/tests/calledmethods/EnsuresCalledMethodsIfTest.java index 2823611b24..3fe5c6c5c6 100644 --- a/checker/tests/calledmethods/EnsuresCalledMethodsIfTest.java +++ b/checker/tests/calledmethods/EnsuresCalledMethodsIfTest.java @@ -7,10 +7,10 @@ class EnsuresCalledMethodsIfTest { @EnsuresCalledMethods(value = "#1", methods = "close") - // The contract is not satisfied. Suppose `sock` is null. Then `sock.close()` throws a - // NullPointerException before `sock.close()` has a chance to be called. The exception is caught - // and control exits the method without `close()` being called. - // :: error: (contracts.postcondition) + // If `sock` is null, `sock.close()` will not be called, and the method will exit normally, as the + // NullPointerException is caught. But, the Called Methods Checker + // assumes the program is free of NullPointerExceptions, delegating verification of that + // property to the Nullness Checker. So, the postcondition is verified. public static void closeSock(EnsuresCalledMethodsIfTest sock) throws Exception { if (!sock.isOpen()) { return; diff --git a/checker/tests/calledmethods/EnsuresCalledMethodsThisLub.java b/checker/tests/calledmethods/EnsuresCalledMethodsThisLub.java new file mode 100644 index 0000000000..f666f946ac --- /dev/null +++ b/checker/tests/calledmethods/EnsuresCalledMethodsThisLub.java @@ -0,0 +1,43 @@ +import org.checkerframework.checker.calledmethods.qual.CalledMethods; +import org.checkerframework.checker.calledmethods.qual.EnsuresCalledMethods; + +class EnsuresCalledMethodsThisLub { + + @EnsuresCalledMethods( + value = "#1", + methods = {"toString", "equals"}) + void call1(Object obj) { + obj.toString(); + obj.equals(null); + } + + @EnsuresCalledMethods( + value = "#1", + methods = {"toString", "hashCode"}) + void call2(Object obj) { + obj.toString(); + obj.hashCode(); + } + + void test(boolean b) { + if (b) { + call1(this); + } else { + call2(this); + } + @CalledMethods("toString") Object obj1 = this; + // :: error: (assignment) + @CalledMethods({"toString", "equals"}) Object obj2 = this; + } + + void test_arg(Object arg, boolean b) { + if (b) { + call1(arg); + } else { + call2(arg); + } + @CalledMethods("toString") Object obj1 = arg; + // :: error: (assignment) + @CalledMethods({"toString", "equals"}) Object obj2 = arg; + } +} diff --git a/checker/tests/calledmethods/Postconditions.java b/checker/tests/calledmethods/Postconditions.java index 41a185e004..73a4c3911a 100644 --- a/checker/tests/calledmethods/Postconditions.java +++ b/checker/tests/calledmethods/Postconditions.java @@ -96,4 +96,37 @@ static void callWithException(Postconditions p) { } catch (java.io.IOException e) { } } + + @EnsuresCalledMethods( + value = {"#1", "#2"}, + methods = "a") + static void callAOnBoth(Postconditions p1, Postconditions p2) { + p1.a(); + p2.a(); + } + + @EnsuresCalledMethods( + value = {"#1", "#2"}, + methods = "a") + static void callAOnBothCatchNPE(Postconditions p1, Postconditions p2) { + // postcondition is verified because the checker assumes NullPointerExceptions cannot occur + try { + p1.a(); + } catch (NullPointerException e) { + } + p2.a(); + } + + @EnsuresCalledMethods( + value = {"#1", "#2"}, + methods = "a") + static int callAOnBothFinallyNPE(Postconditions p1, Postconditions p2) { + // postcondition is verified because the checker assumes NullPointerExceptions cannot occur + try { + p1.a(); + } finally { + p2.a(); + return 0; + } + } } diff --git a/checker/tests/disbaruse-records/DisbarredClass.java b/checker/tests/disbaruse-records/DisbarredClass.java new file mode 100644 index 0000000000..255084d17f --- /dev/null +++ b/checker/tests/disbaruse-records/DisbarredClass.java @@ -0,0 +1,41 @@ +import org.checkerframework.checker.testchecker.disbaruse.qual.DisbarUse; + +class DisbarredClass { + + @DisbarUse String barred; + String fine; + + DisbarredClass() {} + + @DisbarUse + DisbarredClass(String param) {} + + DisbarredClass(@DisbarUse Integer param) {} + + DisbarredClass(@DisbarUse Long param) { + // :: error: (disbar.use) + param = 7L; + // :: error: (disbar.use) + param.toString(); + } + + @DisbarUse + void disbarredMethod() {} + + void invalid() { + // :: error: (disbar.use) + disbarredMethod(); + // :: error: (disbar.use) + new DisbarredClass(""); + // :: error: (disbar.use) + barred = ""; + // :: error: (disbar.use) + int x = barred.length(); + } + + void valid() { + new DisbarredClass(); + fine = ""; + int x = fine.length(); + } +} diff --git a/checker/tests/disbaruse-records/DisbarredRecord.java b/checker/tests/disbaruse-records/DisbarredRecord.java new file mode 100644 index 0000000000..34c9cdae46 --- /dev/null +++ b/checker/tests/disbaruse-records/DisbarredRecord.java @@ -0,0 +1,21 @@ +import org.checkerframework.checker.testchecker.disbaruse.qual.DisbarUse; + +record DisbarredRecord(@DisbarUse String barred, String fine) { + + DisbarredRecord { + // :: error: (disbar.use) + int x = barred.length(); + } + + void invalid() { + // :: error: (disbar.use) + barred(); + // :: error: (disbar.use) + int x = barred.length(); + } + + void valid() { + fine(); + int x = fine.length(); + } +} diff --git a/checker/tests/disbaruse-records/DisbarredRecordByStubs.java b/checker/tests/disbaruse-records/DisbarredRecordByStubs.java new file mode 100644 index 0000000000..13730e728f --- /dev/null +++ b/checker/tests/disbaruse-records/DisbarredRecordByStubs.java @@ -0,0 +1,19 @@ +record DisbarredRecordByStubs(String barred, String fine) { + + DisbarredRecordByStubs { + // :: error: (disbar.use) + int x = barred.length(); + } + + void invalid() { + // :: error: (disbar.use) + barred(); + // :: error: (disbar.use) + int x = barred.length(); + } + + void valid() { + fine(); + int x = fine.length(); + } +} diff --git a/checker/tests/disbaruse-records/DisbarredRecordByStubs2.java b/checker/tests/disbaruse-records/DisbarredRecordByStubs2.java new file mode 100644 index 0000000000..8df03d950a --- /dev/null +++ b/checker/tests/disbaruse-records/DisbarredRecordByStubs2.java @@ -0,0 +1,22 @@ +record DisbarredRecordByStubs2(String barred, String fine) { + + // Annotation isn't copied to explicitly declared constructor parameters: + DisbarredRecordByStubs2(String barred, String fine) { + int x = barred.length(); + // :: error: (disbar.use) + this.barred = ""; + this.fine = fine; + } + + void invalid() { + // :: error: (disbar.use) + barred(); + // :: error: (disbar.use) + int x = barred.length(); + } + + void valid() { + fine(); + int x = fine.length(); + } +} diff --git a/checker/tests/disbaruse-records/disbar.astub b/checker/tests/disbaruse-records/disbar.astub new file mode 100644 index 0000000000..ee6f712dbd --- /dev/null +++ b/checker/tests/disbaruse-records/disbar.astub @@ -0,0 +1,6 @@ +import org.checkerframework.checker.testchecker.disbaruse.qual.DisbarUse; + +record DisbarredRecordByStubs(@DisbarUse String barred, String fine) { +} +record DisbarredRecordByStubs2(@DisbarUse String barred, String fine) { +} diff --git a/checker/tests/guieffect/ThrowCatchTest.java b/checker/tests/guieffect/ThrowCatchTest.java index 92befa91cf..2837e01aa8 100644 --- a/checker/tests/guieffect/ThrowCatchTest.java +++ b/checker/tests/guieffect/ThrowCatchTest.java @@ -4,12 +4,9 @@ import org.checkerframework.checker.guieffect.qual.UI; public class ThrowCatchTest { - // Default type of List's type parameter is below @UI so these - // fields are type.argument.incompatible - // :: error: (type.argument) List ooo; - // :: error: (type.argument) :: error: (annotations.on.use) + // :: error: (annotations.on.use) List iii; class Inner {} @@ -57,14 +54,10 @@ class Inner {} // Wildcards void throwWildcard( - // :: error: (type.argument) - List - ui, // Default type of List's type parameter is below @UI so this is - // type.argument.incompatible + List ui, List alwaysSafe) throws PolyUIException { if (flag) { - // :: error: (throw) throw ui.get(0); } throw alwaysSafe.get(0); diff --git a/checker/tests/interning/ConstantsInterning.java b/checker/tests/interning/ConstantsInterning.java index 24b9d83365..6a5cdb489c 100644 --- a/checker/tests/interning/ConstantsInterning.java +++ b/checker/tests/interning/ConstantsInterning.java @@ -27,6 +27,7 @@ void foo() { // :: error: (assignment) is = is + is; is = Constants2.E; + // :: error: (assignment) is = (String) F; } } diff --git a/checker/tests/interning/StringIntern.java b/checker/tests/interning/StringIntern.java index 29cc943792..ae1a97e2a2 100644 --- a/checker/tests/interning/StringIntern.java +++ b/checker/tests/interning/StringIntern.java @@ -38,7 +38,8 @@ public void test(@Interned String arg) { internedStr = finalStringInitializedToInterned; // OK // :: error: (assignment) internedStr = finalString2; // error - @Interned Foo internedFoo = finalFooInitializedToInterned; // OK + // :: error: (assignment) + @Interned Foo internedFoo = finalFooInitializedToInterned; if (arg == finalStringStatic1) {} // OK // :: error: (not.interned) if (arg == finalStringStatic2) {} // error diff --git a/checker/tests/lock-records/LockRecord.java b/checker/tests/lock-records/LockRecord.java new file mode 100644 index 0000000000..c9f179e6bf --- /dev/null +++ b/checker/tests/lock-records/LockRecord.java @@ -0,0 +1,11 @@ +import java.util.concurrent.locks.ReentrantLock; +import org.checkerframework.checker.lock.qual.LockingFree; + +public record LockRecord(String s, ReentrantLock lock) { + @LockingFree + // :: error: (method.guarantee.violated) + public LockRecord { + // :: error: (method.guarantee.violated) + lock.lock(); + } +} diff --git a/checker/tests/lock/ChapterExamples.java b/checker/tests/lock/ChapterExamples.java index 6ced13ef26..b5b09ff25b 100644 --- a/checker/tests/lock/ChapterExamples.java +++ b/checker/tests/lock/ChapterExamples.java @@ -428,7 +428,7 @@ void myMethod2(@GuardedBy("ChapterExamples.myLock") MyClass e) { int someInt = 1; // TODO: For now, boxed types are treated as primitive types. This may change in the future. - @SuppressWarnings("deprecation") // new Integer + @SuppressWarnings({"deprecation", "removal"}) // new Integer void unboxing() { int a = someInt; // :: error: (immutable.type.guardedby) diff --git a/checker/tests/lock/ViewpointAdaptation.java b/checker/tests/lock/ViewpointAdaptation.java index 74331e40ef..c688048120 100644 --- a/checker/tests/lock/ViewpointAdaptation.java +++ b/checker/tests/lock/ViewpointAdaptation.java @@ -15,8 +15,7 @@ public class ViewpointAdaptation { public void method1(final String a) { synchronized (a) { - // The expression "a" from the @GuardedBy annotation on f is not valid at the declaration site - // of f, but an error was already issued at the declaration of f. + // :: error: (expression.unparsable) f.counter++; } } diff --git a/checker/tests/mustcall/OwningParams.java b/checker/tests/mustcall/OwningParams.java index 7d7f5e05ba..ef995db6a8 100644 --- a/checker/tests/mustcall/OwningParams.java +++ b/checker/tests/mustcall/OwningParams.java @@ -1,19 +1,13 @@ -// Tests that parameters (including receiver parameters) marked as @Owning are still checked. +// Tests that parameters marked as @Owning are still checked. import org.checkerframework.checker.mustcall.qual.*; class OwningParams { static void o1(@Owning OwningParams o) {} - void o2(@Owning OwningParams this) {} - void test(@Owning @MustCall({"a"}) OwningParams o, @Owning OwningParams p) { // :: error: argument o1(o); - // TODO: this error doesn't show up! See MustCallVisitor#skipReceiverSubtypeCheck - // error: method.invocation - o.o2(); o1(p); - p.o2(); } } diff --git a/checker/tests/nullness-extra/issue594/Makefile b/checker/tests/nullness-extra/issue594/Makefile index 9445d4bfb9..1409b4d56b 100644 --- a/checker/tests/nullness-extra/issue594/Makefile +++ b/checker/tests/nullness-extra/issue594/Makefile @@ -2,7 +2,7 @@ all: clean $(JAVAC) -processor org.checkerframework.checker.nullness.NullnessChecker Issue594.java > Out.txt 2>&1 || true - diff Expected.txt Out.txt + diff -u Expected.txt Out.txt clean: rm -f Issue594.class Out.txt diff --git a/checker/tests/nullness-nodelombok/UnsoundnessTest.java b/checker/tests/nullness-nodelombok/UnsoundnessTest.java new file mode 100644 index 0000000000..992e735cac --- /dev/null +++ b/checker/tests/nullness-nodelombok/UnsoundnessTest.java @@ -0,0 +1,17 @@ +// An example of an unsoundness that occurs when running the Nullness Checker +// on Lombok'd code without running delombok first. + +@lombok.Builder +class UnsoundnessTest { + @lombok.NonNull Object foo; + @lombok.NonNull Object bar; + + static void test() { + // An error should be issued here, but the code has not been delombok'd. + // If the CF and Lombok are ever able to work in the same invocation of javac + // (i.e. without delomboking first), then this error should be changed back to an + // expected error by re-adding the leading "::". + // error: (assignment) + builder().foo(null).build(); + } +} diff --git a/checker/tests/nullness-records/BasicRecord.java b/checker/tests/nullness-records/BasicRecord.java new file mode 100644 index 0000000000..a5df9e096d --- /dev/null +++ b/checker/tests/nullness-records/BasicRecord.java @@ -0,0 +1,14 @@ +import org.checkerframework.checker.nullness.qual.Nullable; + +// @below-java17-jdk-skip-test +public record BasicRecord(String str) { + + public static BasicRecord makeNonNull(String s) { + return new BasicRecord(s); + } + + public static BasicRecord makeNull(@Nullable String s) { + // :: error: argument + return new BasicRecord(s); + } +} diff --git a/checker/tests/nullness-records/BasicRecordCanon.java b/checker/tests/nullness-records/BasicRecordCanon.java new file mode 100644 index 0000000000..65965e4c72 --- /dev/null +++ b/checker/tests/nullness-records/BasicRecordCanon.java @@ -0,0 +1,16 @@ +import org.checkerframework.checker.nullness.qual.Nullable; + +// @below-java17-jdk-skip-test +public record BasicRecordCanon(String str) { + + public static BasicRecordCanon makeNonNull(String s) { + return new BasicRecordCanon(s); + } + + public static BasicRecordCanon makeNull(@Nullable String s) { + // :: error: argument + return new BasicRecordCanon(s); + } + + public BasicRecordCanon {} +} diff --git a/checker/tests/nullness-records/BasicRecordNullable.java b/checker/tests/nullness-records/BasicRecordNullable.java new file mode 100644 index 0000000000..5a1c07b30c --- /dev/null +++ b/checker/tests/nullness-records/BasicRecordNullable.java @@ -0,0 +1,31 @@ +import org.checkerframework.checker.nullness.qual.Nullable; + +// @below-java17-jdk-skip-test +public record BasicRecordNullable(@Nullable String str) { + + public static BasicRecordNullable makeNonNull(String s) { + return new BasicRecordNullable(s); + } + + public static BasicRecordNullable makeNull(@Nullable String s) { + return new BasicRecordNullable(s); + } + + public @Nullable String getStringFromField() { + return str; + } + + public @Nullable String getStringFromMethod() { + return str(); + } + + public String getStringFromFieldErr() { + // :: error: return + return str; + } + + public String getStringFromMethodErr() { + // :: error: return + return str(); + } +} diff --git a/checker/tests/nullness-records/DefaultQualRecord.java b/checker/tests/nullness-records/DefaultQualRecord.java new file mode 100644 index 0000000000..c27a9749ec --- /dev/null +++ b/checker/tests/nullness-records/DefaultQualRecord.java @@ -0,0 +1,62 @@ +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.framework.qual.DefaultQualifier; + +class StandardQualClass { + // :: error: assignment + public static String s = null; + // :: error: initialization.static.field.uninitialized + public static String u; +} + +@DefaultQualifier(Nullable.class) +class DefaultQualClass { + public static String s = null; + public static String u; +} + +interface StandardQualInterface { + // :: error: assignment + public static String s = null; +} + +@DefaultQualifier(Nullable.class) +interface DefaultQualInterface { + public static String s = null; +} + +enum StandardQualEnum { + DUMMY; + // :: error: assignment + public static String s = null; + // :: error: initialization.static.field.uninitialized + public static String u; +} + +@DefaultQualifier(Nullable.class) +enum DefaultQualEnum { + DUMMY; + public static String s = null; + public static String u; +} + +record StandardQualRecord(String m) { + // :: error: assignment + public static String s = null; + // :: error: initialization.static.field.uninitialized + public static String u; + + StandardQualRecord { + // :: error: assignment + m = null; + } +} + +@DefaultQualifier(Nullable.class) +record DefaultQualRecord(String m) { + public static String s = null; + public static String u; + + DefaultQualRecord { + m = null; + } +} diff --git a/checker/tests/nullness-records/GenericPair.java b/checker/tests/nullness-records/GenericPair.java new file mode 100644 index 0000000000..a756179581 --- /dev/null +++ b/checker/tests/nullness-records/GenericPair.java @@ -0,0 +1,11 @@ +import org.checkerframework.checker.nullness.qual.Nullable; + +// @below-java17-jdk-skip-test +public record GenericPair(K key, V value) { + + public static void foo() { + GenericPair p = new GenericPair<>("k", null); + // :: error: (dereference.of.nullable) + p.value().toString(); + } +} diff --git a/checker/tests/nullness-records/LocalRecords.java b/checker/tests/nullness-records/LocalRecords.java new file mode 100644 index 0000000000..feea11ca6f --- /dev/null +++ b/checker/tests/nullness-records/LocalRecords.java @@ -0,0 +1,12 @@ +import org.checkerframework.checker.nullness.qual.Nullable; + +// @below-java17-jdk-skip-test +public class LocalRecords { + public static void foo() { + record L(String key, @Nullable Integer value) {} + L a = new L("one", 1); + L b = new L("i", null); + // :: error: (argument) + L c = new L(null, 6); + } +} diff --git a/checker/tests/nullness-records/NestedRecordTest.java b/checker/tests/nullness-records/NestedRecordTest.java new file mode 100644 index 0000000000..3a8b460345 --- /dev/null +++ b/checker/tests/nullness-records/NestedRecordTest.java @@ -0,0 +1,98 @@ +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; + +// @below-java17-jdk-skip-test + +public class NestedRecordTest { + + static @NonNull String nn = "foo"; + static @Nullable String nble = null; + static @NonNull String nn2 = "foo"; + static @Nullable String nble2 = null; + + public static class Nested { + public record NPerson(String familyName, @Nullable String maidenName) {} + + void nclient() { + Nested.NPerson np1 = new Nested.NPerson(nn, nn); + Nested.NPerson np2 = new Nested.NPerson(nn, nble); + // :: error: argument + Nested.NPerson np3 = new Nested.NPerson(nble, nn); + // :: error: argument + Nested.NPerson np4 = new Nested.NPerson(nble, nble); + Inner.IPerson ip1 = new Inner.IPerson(nn, nn); + Inner.IPerson ip2 = new Inner.IPerson(nn, nble); + // :: error: argument + Inner.IPerson ip3 = new Inner.IPerson(nble, nn); + // :: error: argument + Inner.IPerson ip4 = new Inner.IPerson(nble, nble); + + nn2 = np2.familyName(); + nble2 = np2.familyName(); + // :: error: assignment + nn2 = np2.maidenName(); + nble2 = np2.maidenName(); + nn2 = ip2.familyName(); + nble2 = ip2.familyName(); + // :: error: assignment + nn2 = ip2.maidenName(); + nble2 = ip2.maidenName(); + } + } + + public class Inner { + public record IPerson(String familyName, @Nullable String maidenName) {} + + void iclient() { + Nested.NPerson np1 = new Nested.NPerson(nn, nn); + Nested.NPerson np2 = new Nested.NPerson(nn, nble); + // :: error: argument + Nested.NPerson np3 = new Nested.NPerson(nble, nn); + // :: error: argument + Nested.NPerson np4 = new Nested.NPerson(nble, nble); + Inner.IPerson ip1 = new Inner.IPerson(nn, nn); + Inner.IPerson ip2 = new Inner.IPerson(nn, nble); + // :: error: argument + Inner.IPerson ip3 = new Inner.IPerson(nble, nn); + // :: error: argument + Inner.IPerson ip4 = new Inner.IPerson(nble, nble); + + nn2 = np2.familyName(); + nble2 = np2.familyName(); + // :: error: assignment + nn2 = np2.maidenName(); + nble2 = np2.maidenName(); + nn2 = ip2.familyName(); + nble2 = ip2.familyName(); + // :: error: assignment + nn2 = ip2.maidenName(); + nble2 = ip2.maidenName(); + } + } + + void client() { + Nested.NPerson np1 = new Nested.NPerson(nn, nn); + Nested.NPerson np2 = new Nested.NPerson(nn, nble); + // :: error: argument + Nested.NPerson np3 = new Nested.NPerson(nble, nn); + // :: error: argument + Nested.NPerson np4 = new Nested.NPerson(nble, nble); + Inner.IPerson ip1 = new Inner.IPerson(nn, nn); + Inner.IPerson ip2 = new Inner.IPerson(nn, nble); + // :: error: argument + Inner.IPerson ip3 = new Inner.IPerson(nble, nn); + // :: error: argument + Inner.IPerson ip4 = new Inner.IPerson(nble, nble); + + nn2 = np2.familyName(); + nble2 = np2.familyName(); + // :: error: assignment + nn2 = np2.maidenName(); + nble2 = np2.maidenName(); + nn2 = ip2.familyName(); + nble2 = ip2.familyName(); + // :: error: assignment + nn2 = ip2.maidenName(); + nble2 = ip2.maidenName(); + } +} diff --git a/checker/tests/nullness-records/NormalizingRecord.java b/checker/tests/nullness-records/NormalizingRecord.java new file mode 100644 index 0000000000..37ea10974e --- /dev/null +++ b/checker/tests/nullness-records/NormalizingRecord.java @@ -0,0 +1,60 @@ +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; + +// @below-java17-jdk-skip-test + +public class NormalizingRecord {} + +// TODO: Nest the rest of the file within NormalizingRecord when that doesn't crash the Checker +// Framework. + +record NormalizingRecord1(@Nullable String s) { + NormalizingRecord1(String s) { + if (s.equals("")) { + this.s = null; + } else { + this.s = s; + } + } +} + +record NormalizingRecord2(String s) { + NormalizingRecord2(@Nullable String s) { + if (s == null) { + s = ""; + } + this.s = s; + } +} + +record NormalizingRecordIllegalConstructor1(String s) { + NormalizingRecordIllegalConstructor1(@Nullable String s) { + // :: error: (assignment) + this.s = s; + } +} + +record NormalizingRecordIllegalConstructor2(@Nullable String s) { + NormalizingRecordIllegalConstructor2(String s) { + if (s.equals("")) { + // The formal parametr type is @NonNull, so this assignment to it is illegal. + // :: error: (assignment) + s = null; + } + this.s = s; + } +} + +class Client { + + // :: error: (argument) + NormalizingRecord1 nr1_1 = new NormalizingRecord1(null); + NormalizingRecord1 nr1_2 = new NormalizingRecord1(""); + NormalizingRecord1 nr1_3 = new NormalizingRecord1("hello"); + @Nullable String nble = nr1_2.s(); + + NormalizingRecord2 nr2_1 = new NormalizingRecord2(null); + NormalizingRecord2 nr2_2 = new NormalizingRecord2(""); + NormalizingRecord2 nr2_3 = new NormalizingRecord2("hello"); + @NonNull String nn = nr2_1.s(); +} diff --git a/checker/tests/nullness/EnumFieldUninit.java b/checker/tests/nullness/EnumFieldUninit.java new file mode 100644 index 0000000000..791036e92b --- /dev/null +++ b/checker/tests/nullness/EnumFieldUninit.java @@ -0,0 +1,23 @@ +enum EnumFieldUninit { + DUMMY; + + // :: error: (assignment) + public static String s = null; + + // :: error: (initialization.static.field.uninitialized) + public static String u; + + static String[] arrayInit = new String[] {}; + + static String[] arrayInitInBlock; + + static { + arrayInitInBlock = new String[] {}; + } + + // :: error: (assignment) + static String[] arrayInitToNull = null; + + // :: error: (initialization.static.field.uninitialized) + static String[] arrayUninit; +} diff --git a/checker/tests/nullness/FinalFields.java b/checker/tests/nullness/FinalFields.java index 9f82333c84..e7c0febcce 100644 --- a/checker/tests/nullness/FinalFields.java +++ b/checker/tests/nullness/FinalFields.java @@ -2,13 +2,13 @@ class Upper { @Nullable String fs = "NonNull init"; - final @Nullable String ffs = "NonNull init"; + private final @Nullable String ffs = "NonNull init"; void access() { // Error, because non-final field type is not refined // :: error: (dereference.of.nullable) fs.hashCode(); - // Final field in the same class is refined + // private final field is refined ffs.hashCode(); } } @@ -35,13 +35,13 @@ public void local() { class Lower { @Nullable String fs = "NonNull init, too"; - final @Nullable String ffs = "NonNull init, too"; + private final @Nullable String ffs = "NonNull init, too"; void access() { // Error, because non-final field type is not refined // :: error: (dereference.of.nullable) fs.hashCode(); - // Final field in the same class is refined + // private final field is refined ffs.hashCode(); } } diff --git a/checker/tests/nullness/FlowField.java b/checker/tests/nullness/FlowField.java index fa46ce3985..2663e1f87c 100644 --- a/checker/tests/nullness/FlowField.java +++ b/checker/tests/nullness/FlowField.java @@ -27,7 +27,7 @@ void test1() { nonnull.toString(); } - static final String nonnull = new String(); + private static final String nonnull = new String(); class A { protected String field = null; diff --git a/checker/tests/nullness/Initializer.java b/checker/tests/nullness/Initializer.java index c72846d036..74d759dc72 100644 --- a/checker/tests/nullness/Initializer.java +++ b/checker/tests/nullness/Initializer.java @@ -13,6 +13,7 @@ public class Initializer { public String d = (""); + // :: error: (initialization.fields.uninitialized) public Initializer() { // :: error: (assignment) a = null; @@ -26,11 +27,13 @@ public Initializer(boolean foo) {} public Initializer(int foo) { a = ""; c = ""; + f = ""; } public Initializer(float foo) { setField(); c = ""; + f = ""; } public Initializer(double foo) { @@ -38,6 +41,7 @@ public Initializer(double foo) { a = ""; } c = ""; + f = ""; } // :: error: (initialization.fields.uninitialized) @@ -59,7 +63,7 @@ public boolean setFieldMaybe(@UnknownInitialization Initializer this) { return true; } - String f = ""; + String f; void t1(@UnknownInitialization Initializer this) { // :: error: (dereference.of.nullable) @@ -71,7 +75,8 @@ void t1(@UnknownInitialization Initializer this) { class SubInitializer extends Initializer { - String f = ""; + // :: error: (initialization.field.uninitialized) + String f; void subt1(@UnknownInitialization(Initializer.class) SubInitializer this) { fieldF.toString(); diff --git a/checker/tests/nullness/Issue1027.java b/checker/tests/nullness/Issue1027.java index 48a445d93c..89822314bc 100644 --- a/checker/tests/nullness/Issue1027.java +++ b/checker/tests/nullness/Issue1027.java @@ -19,7 +19,7 @@ class Repr { void bar(Function p) {} } - @SuppressWarnings("nullness") + @SuppressWarnings({"nullness", "keyfor"}) Repr<@KeyFor("this") String> foo() { return null; } diff --git a/checker/tests/nullness/Issue2048.java b/checker/tests/nullness/Issue2048.java index 115d5e90ca..020aa2d0cd 100644 --- a/checker/tests/nullness/Issue2048.java +++ b/checker/tests/nullness/Issue2048.java @@ -4,18 +4,25 @@ // There are two versions: // framework/tests/all-systems // checker/tests/nullness +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; public class Issue2048 { interface Foo {} - interface Fooer {} + static class Fooer {} class UseNbl { - // T by default is @Nullable and therefore doesn't - // fulfill the bound of R. - // :: error: (type.argument) void foo(Fooer fooer) {} } + // :: error: (type.argument) + Fooer<@Nullable Foo> nblFooer = new Fooer<>(); + Fooer<@NonNull Foo> nnFooer = new Fooer<>(); + + void use(UseNbl<@Nullable Foo> useNbl) { + useNbl.foo(nblFooer); + useNbl.foo(nnFooer); + } class UseNN { void foo(Fooer fooer) {} diff --git a/checker/tests/nullness/Issue3013.java b/checker/tests/nullness/Issue3013.java new file mode 100644 index 0000000000..0611ddf03d --- /dev/null +++ b/checker/tests/nullness/Issue3013.java @@ -0,0 +1,11 @@ +import org.checkerframework.checker.nullness.qual.NonNull; + +abstract class Issue3013 { + static class Nested { + Nested(Issue3013 list) { + for (Object o : list.asIterable()) {} + } + } + + abstract Iterable asIterable(); +} diff --git a/checker/tests/nullness/Issue3754.java b/checker/tests/nullness/Issue3754.java new file mode 100644 index 0000000000..b26d726326 --- /dev/null +++ b/checker/tests/nullness/Issue3754.java @@ -0,0 +1,19 @@ +import org.checkerframework.checker.nullness.qual.Nullable; + +class Issue3754 { + interface Supplier { + U get(); + } + + Object x(Supplier bar) { + return bar.get(); + } + + interface Supplier2 { + U get(); + } + + Object x(Supplier2 bar) { + return bar.get(); + } +} diff --git a/checker/tests/nullness/Issue3845.java b/checker/tests/nullness/Issue3845.java new file mode 100644 index 0000000000..b30f64c37f --- /dev/null +++ b/checker/tests/nullness/Issue3845.java @@ -0,0 +1,30 @@ +package wildcards; + +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.framework.qual.DefaultQualifier; +import org.checkerframework.framework.qual.TypeUseLocation; + +public class Issue3845 { + static class Holder { + final T value; + + Holder(T value) { + this.value = value; + } + } + + interface HolderSupplier> { + H get(); + } + + @DefaultQualifier(value = Nullable.class, locations = TypeUseLocation.ALL) + static class DefaultClash { + + Object go(HolderSupplier s) { + if (s != null) { + return s.get(); + } + return ""; + } + } +} diff --git a/checker/tests/nullness/Issue3970.java b/checker/tests/nullness/Issue3970.java index bd55ef2290..3710d99c49 100644 --- a/checker/tests/nullness/Issue3970.java +++ b/checker/tests/nullness/Issue3970.java @@ -12,6 +12,7 @@ public interface InterfaceB> { void t(InterfaceA a) { if (a.f() == 1) { + // :: error: (assignment) InterfaceA a2 = a.g(); } } diff --git a/checker/tests/nullness/Issue4523.java b/checker/tests/nullness/Issue4523.java new file mode 100644 index 0000000000..bf1ba98bc0 --- /dev/null +++ b/checker/tests/nullness/Issue4523.java @@ -0,0 +1,14 @@ +import org.checkerframework.checker.nullness.qual.Nullable; + +public class Issue4523 { + + interface InterfaceA> extends InterfaceB {} + + interface InterfaceB> { + @Nullable T g(); + } + + void f(InterfaceA x) { + InterfaceA y = x.g() != null ? x.g() : x; + } +} diff --git a/checker/tests/nullness/Issue4853Nullness.java b/checker/tests/nullness/Issue4853Nullness.java new file mode 100644 index 0000000000..2800a9b941 --- /dev/null +++ b/checker/tests/nullness/Issue4853Nullness.java @@ -0,0 +1,18 @@ +import org.checkerframework.checker.nullness.qual.Nullable; + +public class Issue4853Nullness { + interface Interface {} + + static class MyClass { + class InnerMyClass implements Interface {} + } + + abstract static class SubMyClass extends MyClass<@Nullable String> { + protected void f() { + // :: error: (argument) + method(new InnerMyClass()); + } + + abstract void method(Interface callback); + } +} diff --git a/checker/tests/nullness/Issue4889.java b/checker/tests/nullness/Issue4889.java new file mode 100644 index 0000000000..f30aa01677 --- /dev/null +++ b/checker/tests/nullness/Issue4889.java @@ -0,0 +1,14 @@ +import java.util.Objects; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; + +class Issue4889 { + void f(@Nullable String s) { + Objects.toString(s, "").toString(); + } + + void g(@Nullable String s) { + @NonNull String x = Objects.toString(s, ""); + @Nullable String y = Objects.toString(s, null); + } +} diff --git a/checker/tests/nullness/Issue4924.java b/checker/tests/nullness/Issue4924.java new file mode 100644 index 0000000000..1cece326ab --- /dev/null +++ b/checker/tests/nullness/Issue4924.java @@ -0,0 +1,27 @@ +// Test case for issue #4924: https://tinyurl.com/cfissue/4924 + +// @skip-test until the issue is fixed + +class Issue4924 {} + +interface Callback4924 {} + +class Template4924 { + interface Putter4924 { + void put(T result); + } + + class Adapter4924 implements Callback4924 { + Adapter4924(Putter4924 putter) {} + } +} + +class Super4924 extends Template4924 {} + +class Issue extends Super4924 { + void go(Callback4924 callback) {} + + void foo() { + go(new Adapter4924(result -> {})); + } +} diff --git a/checker/tests/nullness/Iterate.java b/checker/tests/nullness/Iterate.java new file mode 100644 index 0000000000..d36975723f --- /dev/null +++ b/checker/tests/nullness/Iterate.java @@ -0,0 +1,9 @@ +package wildcards; + +public class Iterate { + void method(Iterable files) { + for (Object file : files) { + file.getClass(); + } + } +} diff --git a/checker/tests/nullness/KeyForLub.java b/checker/tests/nullness/KeyForLub.java index ef0e343d1e..dc77bc2ef3 100644 --- a/checker/tests/nullness/KeyForLub.java +++ b/checker/tests/nullness/KeyForLub.java @@ -27,7 +27,7 @@ void method( } @PolyKeyFor String poly1(@KeyFor("map1") String key1, @PolyKeyFor String poly) { - // :: error: (return) + // :: error: (conditional) return flag ? key1 : poly; } diff --git a/checker/tests/nullness/NullnessIssue4996.java b/checker/tests/nullness/NullnessIssue4996.java new file mode 100644 index 0000000000..78600f6d7d --- /dev/null +++ b/checker/tests/nullness/NullnessIssue4996.java @@ -0,0 +1,21 @@ +class NullnessIssue4996 { + abstract class CaptureOuter { + abstract T get(); + + abstract class Inner { + abstract T get(); + } + } + + class Client { + Object getFrom(CaptureOuter o) { + // :: error: (return) + return o.get(); + } + + Object getFrom(CaptureOuter.Inner o) { + // :: error: (return) + return o.get(); + } + } +} diff --git a/checker/tests/nullness/OverrideGenerics.java b/checker/tests/nullness/OverrideGenerics.java index 920f016c9e..893905ea24 100644 --- a/checker/tests/nullness/OverrideGenerics.java +++ b/checker/tests/nullness/OverrideGenerics.java @@ -1,13 +1,13 @@ import org.checkerframework.checker.nullness.qual.*; -class Super { +class OGSuper { public void m(S p) {} } -class Impl1 extends Super { +class OGImpl1 extends OGSuper { public void m(T p) {} } -class Impl2 extends Super { +class OGImpl2 extends OGSuper { public void m(T p) {} } diff --git a/checker/tests/nullness/RepeatedRequiresNonNull.java b/checker/tests/nullness/RepeatedRequiresNonNull.java new file mode 100644 index 0000000000..7bb1e56ca8 --- /dev/null +++ b/checker/tests/nullness/RepeatedRequiresNonNull.java @@ -0,0 +1,78 @@ +// A test that multiple @RequiresNonNull annotations can be written on the same +// method and work correctly. + +import org.checkerframework.checker.nullness.qual.*; +import org.checkerframework.framework.qual.RequiresQualifier; + +class RepeatedRequiresNonNull { + @Nullable Object f1; + @Nullable Object f2; + + @RequiresNonNull("this.f1") + @RequiresNonNull("this.f2") + void test() { + f1.toString(); + f2.toString(); + } + + void use1() { + // :: error: (contracts.precondition) + test(); + } + + void use2() { + if (this.f1 != null) { + // :: error: (contracts.precondition) + test(); + } + } + + void use3() { + if (this.f2 != null) { + // :: error: (contracts.precondition) + test(); + } + } + + void use4() { + if (this.f1 != null && this.f2 != null) { + test(); + } + } + + // This part of the test is to ensure that @RequiresNonNull and @RequiresQualifier behave + // the same way. It is identical, but uses @RequiresQualifier on the test2() method instead + // of the @RequiresNonNull on the test() method. + + @RequiresQualifier(expression = "this.f1", qualifier = NonNull.class) + @RequiresQualifier(expression = "this.f2", qualifier = NonNull.class) + void test2() { + f1.toString(); + f2.toString(); + } + + void use21() { + // :: error: (contracts.precondition) + test2(); + } + + void use22() { + if (this.f1 != null) { + // :: error: (contracts.precondition) + test2(); + } + } + + void use23() { + if (this.f2 != null) { + // :: error: (contracts.precondition) + test2(); + } + } + + void use24() { + if (this.f1 != null && this.f2 != null) { + test2(); + } + } +} diff --git a/checker/tests/nullness/WildcardGLB.java b/checker/tests/nullness/WildcardGLB.java new file mode 100644 index 0000000000..6b45570380 --- /dev/null +++ b/checker/tests/nullness/WildcardGLB.java @@ -0,0 +1,68 @@ +import java.util.ArrayList; +import java.util.List; +import org.checkerframework.checker.nullness.qual.Nullable; + +public class WildcardGLB { + + static class MyClass> { + E getE() { + throw new RuntimeException(); + } + } + + // The captured type variable for + // ? extends @List<@NonNull String> + // is + // capture#865 extends @NonNull List<@Nullable String> + // . The upper bound of + // the captured type variable is not a subtype of the extends bound of the + // wildcard because the glb of the type parameter bound and the wildcard + // extends bound does not exist. I don't think this leads to unsoundness, + // but it makes it so that this method can't be called without an error. The + // method testUse below demos this. + // :: error: (type.argument) + void use(MyClass> s) { + // :: error: (assignment) + List f = s.getE(); + List<@Nullable String> f2 = s.getE(); + } + + void testUse( + // :: error: (type.argument) + MyClass> p1, + // A comment to force a line break. + MyClass> p2) { + use(p1); + // :: error: (argument) + use(p2); + } + + // capture#196 extends @NonNull ArrayList<@NonNull String> + // :: error: (type.argument) + void use2(MyClass> s) { // error: type.argument + List f = s.getE(); + // :: error: (assignment) + List<@Nullable String> f2 = s.getE(); // error: assignment + } + + static class MyClass2> { + E getE() { + throw new RuntimeException(); + } + } + + // capture#952 extends @NonNull ArrayList<@Nullable String> + // :: error: (type.argument) + void use3(MyClass2> s) { + // :: error: (assignment) + List f = s.getE(); + List<@Nullable String> f2 = s.getE(); + } + + // :: error: (type.argument) + void use4(MyClass2> s) { + // :: error: assignment + List f = s.getE(); + List<@Nullable String> f2 = s.getE(); // ok + } +} diff --git a/checker/tests/nullness/generics/CapturedWildcards.java b/checker/tests/nullness/generics/CapturedWildcards.java new file mode 100644 index 0000000000..f193812ccf --- /dev/null +++ b/checker/tests/nullness/generics/CapturedWildcards.java @@ -0,0 +1,17 @@ +import java.util.List; +import org.checkerframework.checker.nullness.qual.Nullable; + +public class CapturedWildcards { + abstract static class MyClass { + abstract boolean contains(MyClass other); + } + + public boolean pass(List list, MyClass other) { + return list.stream().anyMatch(je -> je != null && je.contains(other)); + } + + public boolean fail(List list, MyClass other) { + // :: error: (dereference.of.nullable) + return list.stream().anyMatch(je -> je.contains(other)); + } +} diff --git a/checker/tests/nullness/generics/WildcardOverride.java b/checker/tests/nullness/generics/WildcardOverride.java index 9023785cdd..243ecc4dcb 100644 --- a/checker/tests/nullness/generics/WildcardOverride.java +++ b/checker/tests/nullness/generics/WildcardOverride.java @@ -6,30 +6,22 @@ import org.checkerframework.checker.nullness.qual.NonNull; interface ToOverride { - // For nullness this should default to type @NonNull List public abstract int transform(List function); } public class WildcardOverride implements ToOverride { - // invalid because the overriden method takes @Nullable args and this one doesn't @Override - // :: error: (override.param) public int transform(List function) { return 0; } } interface ToOverride2 { - // For nullness this should be typed as - // @NonNull List // :: error: (bound) public abstract int transform(List<@NonNull ? super T> function); } class WildcardOverride2 implements ToOverride2 { - // valid because the overriden method takes ONLY @NonNull args and this one takes @NonNull args - // as well @Override public int transform(List function) { return 0; diff --git a/checker/tests/nullness/generics/WildcardSubtyping.java b/checker/tests/nullness/generics/WildcardSubtyping.java index f483283d1a..116edd150a 100644 --- a/checker/tests/nullness/generics/WildcardSubtyping.java +++ b/checker/tests/nullness/generics/WildcardSubtyping.java @@ -56,7 +56,6 @@ class UseMyGeneric { class MyGenericExactBounds<@NonNull T extends @NonNull Number> {} class UseMyGenericExactBounds { - // :: error: (type.argument) MyGenericExactBounds wildcardOutsideUBError = new MyGenericExactBounds<>(); MyGenericExactBounds wildcardOutside = new MyGenericExactBounds<>(); diff --git a/checker/tests/nullness/generics/WildcardSuper.java b/checker/tests/nullness/generics/WildcardSuper.java index 7e0dc558ce..8629e8bc2a 100644 --- a/checker/tests/nullness/generics/WildcardSuper.java +++ b/checker/tests/nullness/generics/WildcardSuper.java @@ -4,19 +4,11 @@ public class WildcardSuper { void testWithSuper(Cell cell) { - // TODO: Address comments. Since ? is explicitly lower bounded, I have made a judgment that - // it should be implicitly upper bounded. - // This is valid because the default upper bound is NonNull // :: error: (dereference.of.nullable) cell.get().toString(); } - // TODO: THIS SHOULD JUST ISSUE A WARNING, WHY WOULD PEOPLE WANT TO WRITE CONTRADICTING BOUNDS? void testWithContradiction(Cell cell) { - // This is actually valid, because it's a contradiction, because - // the implicit upper bound is NonNull. - // We are free to do anything, as the method is not callable. - // TODO: test whether all calls of method fail. // :: error: (dereference.of.nullable) cell.get().toString(); } diff --git a/checker/tests/nullness/java-unsound/Figure3.java b/checker/tests/nullness/java-unsound/Figure3.java index 190a6949eb..6e3a6529df 100644 --- a/checker/tests/nullness/java-unsound/Figure3.java +++ b/checker/tests/nullness/java-unsound/Figure3.java @@ -8,6 +8,10 @@ Constraint bad() { } A coerce(B b) { + // type of expression: capture#703[ extends @Initialized @Nullable Object super B[ + // extends @Initialized @Nullable Object super @Initialized @NonNull Void]] + // method return type: A[ extends @Initialized @Nullable Object super @Initialized + // @NonNull Void] return pair(this.bad(), b).value; } } diff --git a/checker/tests/nullness/java17/NullnessSwitchArrows.java b/checker/tests/nullness/java17/NullnessSwitchArrows.java new file mode 100644 index 0000000000..08983be671 --- /dev/null +++ b/checker/tests/nullness/java17/NullnessSwitchArrows.java @@ -0,0 +1,78 @@ +// @below-java17-jdk-skip-test +public class NullnessSwitchArrows { + public enum Day { + SUNDAY, + MONDAY, + TUESDAY, + WEDNESDAY, + THURSDAY, + FRIDAY, + SATURDAY; + } + + void method1() { + Object o; + Day day = Day.WEDNESDAY; + switch (day) { + case MONDAY, FRIDAY, SUNDAY -> o = "hello"; + case TUESDAY -> o = null; + case THURSDAY, SATURDAY -> o = "hello"; + case WEDNESDAY -> o = "hello"; + default -> throw new IllegalStateException("Invalid day: " + day); + } + + // :: error: (dereference.of.nullable) + o.toString(); + } + + void method2() { + Object o; + Day day = Day.WEDNESDAY; + switch (day) { + case MONDAY, FRIDAY, SUNDAY -> o = "hello"; + case TUESDAY -> o = "hello"; + case THURSDAY, SATURDAY -> o = "hello"; + case WEDNESDAY -> o = "hello"; + default -> throw new IllegalStateException("Invalid day: " + day); + } + + o.toString(); + } + + void method2b() { + Object o; + Day day = Day.WEDNESDAY; + switch (day) { + case MONDAY, FRIDAY, SUNDAY: + o = "hello"; + break; + case TUESDAY: + o = "hello"; + break; + case THURSDAY, SATURDAY: + o = "hello"; + break; + case WEDNESDAY: + o = "hello"; + break; + default: + throw new IllegalStateException("Invalid day: " + day); + } + + o.toString(); + } + + void method3() { + Object o; + Day day = Day.WEDNESDAY; + switch (day) { + case MONDAY, FRIDAY, SUNDAY -> o = "hello"; + case TUESDAY -> o = "hello"; + case THURSDAY, SATURDAY -> o = "hello"; + case WEDNESDAY -> o = "hello"; + default -> o = "hello"; + } + + o.toString(); + } +} diff --git a/checker/tests/nullness/java17/NullnessSwitchExpressionLambda.java b/checker/tests/nullness/java17/NullnessSwitchExpressionLambda.java new file mode 100644 index 0000000000..61685d40dd --- /dev/null +++ b/checker/tests/nullness/java17/NullnessSwitchExpressionLambda.java @@ -0,0 +1,19 @@ +// @below-java17-jdk-skip-test +import java.util.function.Function; +import org.checkerframework.checker.nullness.qual.Nullable; + +public class NullnessSwitchExpressionLambda { + int anInt; + + void switchExprLambda() { + Function f = + (n) -> + switch (n.anInt) { + case 3, 4, 5 -> new Object(); + default -> null; + }; + Object o = f.apply(new NullnessSwitchExpressionLambda()); + // :: error: (dereference.of.nullable) + o.toString(); + } +} diff --git a/checker/tests/nullness/java17/NullnessSwitchExpressions.java b/checker/tests/nullness/java17/NullnessSwitchExpressions.java new file mode 100644 index 0000000000..ecf5accb6b --- /dev/null +++ b/checker/tests/nullness/java17/NullnessSwitchExpressions.java @@ -0,0 +1,63 @@ +// @below-java17-jdk-skip-test +public class NullnessSwitchExpressions { + public enum Day { + SUNDAY, + MONDAY, + TUESDAY, + WEDNESDAY, + THURSDAY, + FRIDAY, + SATURDAY; + } + + void method1() { + Day day = Day.WEDNESDAY; + Object o = + switch (day) { + case MONDAY, FRIDAY, SUNDAY -> "hello"; + case TUESDAY -> null; + case THURSDAY, SATURDAY -> "hello"; + case WEDNESDAY -> "hello"; + default -> throw new IllegalStateException("Invalid day: " + day); + }; + + // :: error: (dereference.of.nullable) + o.toString(); + } + + void method2() { + Day day = Day.WEDNESDAY; + Object o = + switch (day) { + case MONDAY, FRIDAY, SUNDAY -> "hello"; + case TUESDAY -> "hello"; + case THURSDAY, SATURDAY -> "hello"; + case WEDNESDAY -> "hello"; + default -> throw new IllegalStateException("Invalid day: " + day); + }; + + o.toString(); + } + + void method3() { + Day day = Day.WEDNESDAY; + Object o = + switch (day) { + case MONDAY, FRIDAY, SUNDAY -> "hello"; + case TUESDAY -> "hello"; + case THURSDAY, SATURDAY -> { + String s = null; + if (day == Day.THURSDAY) { + s = "hello"; + s.toString(); + } + yield s; + } + case WEDNESDAY -> "hello"; + default -> throw new IllegalStateException("Invalid day: " + day); + }; + + // :: error: (dereference.of.nullable) + o.toString(); + } +} diff --git a/checker/tests/nullness/java17/NullnessSwitchStatementRules.java b/checker/tests/nullness/java17/NullnessSwitchStatementRules.java new file mode 100644 index 0000000000..9c375f64cb --- /dev/null +++ b/checker/tests/nullness/java17/NullnessSwitchStatementRules.java @@ -0,0 +1,43 @@ +// @below-java17-jdk-skip-test +import org.checkerframework.checker.nullness.qual.Nullable; + +public class NullnessSwitchStatementRules { + @Nullable Object field = null; + + void method(int selector) { + field = new Object(); + switch (selector) { + case 1 -> field = null; + case 2 -> field.toString(); + } + + field = new Object(); + switch (selector) { + case 1 -> { + field = null; + } + case 2 -> { + field.toString(); + } + } + + field = new Object(); + switch (selector) { + case 1 -> { + field = null; + } + case 2 -> { + field.toString(); + } + } + + field = new Object(); + switch (selector) { + case 1: + field = null; + case 2: + // :: error: (dereference.of.nullable) + field.toString(); + } + } +} diff --git a/checker/tests/nullness/java17/SwitchExpressionInvariant.java b/checker/tests/nullness/java17/SwitchExpressionInvariant.java new file mode 100644 index 0000000000..6be0fa35fd --- /dev/null +++ b/checker/tests/nullness/java17/SwitchExpressionInvariant.java @@ -0,0 +1,27 @@ +// @below-java17-jdk-skip-test +import java.util.List; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; + +public class SwitchExpressionInvariant { + public static boolean flag = false; + + void method( + List<@NonNull String> nonnullStrings, List<@Nullable String> nullableStrings, int fenum) { + + List<@NonNull String> list = + // :: error: (assignment) + switch (fenum) { + // :: error: (switch.expression) + case 1 -> nonnullStrings; + default -> nullableStrings; + }; + + List<@Nullable String> list2 = + switch (fenum) { + // :: error: (switch.expression) + case 1 -> nonnullStrings; + default -> nullableStrings; + }; + } +} diff --git a/checker/tests/nullness/java8/Issue1098.java b/checker/tests/nullness/java8/Issue1098.java index da8f891e16..2b5f3a74a9 100644 --- a/checker/tests/nullness/java8/Issue1098.java +++ b/checker/tests/nullness/java8/Issue1098.java @@ -8,6 +8,7 @@ void opt(Optional p1, T p2) {} void cls(Class p1, T p2) {} + @SuppressWarnings("keyfor:type.argument") void use() { opt(Optional.empty(), null); // TODO: false positive, because type argument inference does not account for @Covariant. diff --git a/checker/tests/nullness/java8/methodref/GroundTargetTypeLub.java b/checker/tests/nullness/java8/methodref/GroundTargetTypeLub.java index 9fd60dcb03..9e1625169a 100644 --- a/checker/tests/nullness/java8/methodref/GroundTargetTypeLub.java +++ b/checker/tests/nullness/java8/methodref/GroundTargetTypeLub.java @@ -14,7 +14,6 @@ class GroundTargetType { return null; } - // :: error: (type.argument) Supplier fn = GroundTargetType::myMethod; // :: error: (methodref.return) Supplier fn2 = GroundTargetType::myMethod; diff --git a/checker/tests/nullness/java8inference/Issue1084.java b/checker/tests/nullness/java8inference/Issue1084.java index b0fd270678..89090458ae 100644 --- a/checker/tests/nullness/java8inference/Issue1084.java +++ b/checker/tests/nullness/java8inference/Issue1084.java @@ -7,7 +7,7 @@ class MyOpt { static MyOpt<@NonNull S> empty() { throw new RuntimeException(); } - + // :: error: (type.argument) static MyOpt of(S p) { throw new RuntimeException(); } diff --git a/checker/tests/regex/RawTypeTest.java b/checker/tests/regex/RawTypeTest.java index 332d42f77b..bdecc09d2d 100644 --- a/checker/tests/regex/RawTypeTest.java +++ b/checker/tests/regex/RawTypeTest.java @@ -53,6 +53,7 @@ public void m3(Class c) { m2(c); } + @SuppressWarnings("removal") // AccessController is deprecated for removal in Java 17 public void m4() { AccessController.doPrivileged( new PrivilegedAction() { diff --git a/checker/tests/resourceleak-nocreatesmustcallfor/DifferentSWKeys.java b/checker/tests/resourceleak-nocreatesmustcallfor/DifferentSWKeys.java new file mode 100644 index 0000000000..b9d626f214 --- /dev/null +++ b/checker/tests/resourceleak-nocreatesmustcallfor/DifferentSWKeys.java @@ -0,0 +1,26 @@ +// A test case that the RLC's -AnoCreatesMustCallFor argument doesn't change +// warning suppression behavior for the Must Call Checker. + +import org.checkerframework.checker.mustcall.qual.MustCall; +import org.checkerframework.checker.mustcall.qual.Owning; + +@SuppressWarnings("required.method.not.called") +class DifferentSWKeys { + void test(@Owning @MustCall("foo") Object obj) { + // :: warning: unneeded.suppression + @SuppressWarnings("mustcall") + @MustCall("foo") Object bar = obj; + } + + void test2(@Owning @MustCall("foo") Object obj) { + // actually needed suppression + @SuppressWarnings("mustcall") + @MustCall({}) Object bar = obj; + } + + void test3(@Owning @MustCall("foo") Object obj) { + // test that the option-specific suppression key works + @SuppressWarnings("mustcallnocreatesmustcallfor") + @MustCall({}) Object bar = obj; + } +} diff --git a/checker/tests/resourceleak/ACRegularExitPointTest.java b/checker/tests/resourceleak/ACRegularExitPointTest.java index a7bb586312..f90b1297e2 100644 --- a/checker/tests/resourceleak/ACRegularExitPointTest.java +++ b/checker/tests/resourceleak/ACRegularExitPointTest.java @@ -299,102 +299,4 @@ void testSubFoo2() { // :: error: required.method.not.called SubFoo f = new SubFoo(); } - - static void takeOwnership(@Owning Foo foo) { - foo.a(); - } - - /** cases where ternary expressions are assigned to a variable */ - void testTernaryAssigned(boolean b) { - Foo ternary1 = b ? new Foo() : makeFoo(); - ternary1.a(); - - // :: error: required.method.not.called - Foo ternary2 = b ? new Foo() : makeFoo(); - - // :: error: required.method.not.called - Foo x = new Foo(); - Foo ternary3 = b ? new Foo() : x; - ternary3.a(); - - Foo y = new Foo(); - Foo ternary4 = b ? y : y; - ternary4.a(); - - takeOwnership(b ? new Foo() : makeFoo()); - - // :: error: required.method.not.called - Foo x2 = new Foo(); - takeOwnership(b ? x2 : null); - - int i = 10; - Foo ternaryInLoop = null; - while (i > 0) { - // :: error: required.method.not.called - ternaryInLoop = b ? null : new Foo(); - i--; - } - ternaryInLoop.a(); - - (b ? new Foo() : makeFoo()).a(); - } - - /** - * tests where ternary and cast expressions (possibly nested) may or may not be assigned to a - * variable - */ - void testTernaryCastUnassigned(boolean b) { - // :: error: required.method.not.called - if ((b ? new Foo() : null) != null) { - b = !b; - } - - // :: error: required.method.not.called - if ((b ? makeFoo() : null) != null) { - b = !b; - } - - Foo x = new Foo(); - if ((b ? x : null) != null) { - b = !b; - } - x.a(); - - // :: error: required.method.not.called - if (((Foo) new Foo()) != null) { - b = !b; - } - - // double cast; no error - Foo doubleCast = (Foo) ((Foo) makeFoo()); - doubleCast.a(); - - // nesting casts and ternary expressions; no error - Foo deepNesting = (b ? (!b ? makeFoo() : (Foo) makeFoo()) : ((Foo) new Foo())); - deepNesting.a(); - } - - @Owning - Foo testTernaryReturnOk(boolean b) { - return b ? new Foo() : makeFoo(); - } - - @Owning - Foo testTernaryReturnBad(boolean b) { - // :: error: required.method.not.called - Foo x = new Foo(); - return b ? x : makeFoo(); - } - - @MustCall("toString") static class Sub1 extends Object {} - - @MustCall("clone") static class Sub2 extends Object {} - - static void test_ternary(boolean b) { - // :: error: required.method.not.called - Object toStringAndClone = b ? new Sub1() : new Sub2(); - // at this point, for soundness, we should be responsible for calling both toString and clone on - // obj... - toStringAndClone.toString(); - } } diff --git a/checker/tests/resourceleak/DifferentSWKeys.java b/checker/tests/resourceleak/DifferentSWKeys.java new file mode 100644 index 0000000000..9f72279f07 --- /dev/null +++ b/checker/tests/resourceleak/DifferentSWKeys.java @@ -0,0 +1,27 @@ +// A test case that the RLC's -AnoCreatesMustCallFor argument doesn't change +// warning suppression behavior for the Must Call Checker. + +import org.checkerframework.checker.mustcall.qual.MustCall; +import org.checkerframework.checker.mustcall.qual.Owning; + +@SuppressWarnings("required.method.not.called") +class DifferentSWKeys { + void test(@Owning @MustCall("foo") Object obj) { + // :: warning: unneeded.suppression + @SuppressWarnings("mustcall") + @MustCall("foo") Object bar = obj; + } + + void test2(@Owning @MustCall("foo") Object obj) { + // actually needed suppression + @SuppressWarnings("mustcall") + @MustCall({}) Object bar = obj; + } + + void test3(@Owning @MustCall("foo") Object obj) { + // test that the option-specific suppression key doesn't work + @SuppressWarnings("mustcallnocreatesmustcallfor") + // :: error: assignment + @MustCall({}) Object bar = obj; + } +} diff --git a/checker/tests/resourceleak/IgnoredExceptionECM.java b/checker/tests/resourceleak/IgnoredExceptionECM.java new file mode 100644 index 0000000000..7d711db85e --- /dev/null +++ b/checker/tests/resourceleak/IgnoredExceptionECM.java @@ -0,0 +1,19 @@ +// A test that the Resource Leak Checker ignores exceptions in destructors the same way that it +// does in the consistency checker. + +import org.checkerframework.checker.calledmethods.qual.EnsuresCalledMethods; +import org.checkerframework.checker.mustcall.qual.*; + +@MustCall("foo") class IgnoredExceptionECM { + + @Owning + @MustCall("toString") Object obj; + + @EnsuresCalledMethods(value = "this.obj", methods = "toString") + void foo() { + // This line will produce an exception, + // which the RLC should ignore and verify the method. + int y = 5 / 0; + this.obj.toString(); + } +} diff --git a/checker/tests/resourceleak/InstanceInitializer.java b/checker/tests/resourceleak/InstanceInitializer.java new file mode 100644 index 0000000000..1a5abae0ef --- /dev/null +++ b/checker/tests/resourceleak/InstanceInitializer.java @@ -0,0 +1,50 @@ +// A test that the checker is sound in the presence of instance initializer blocks. + +import java.net.Socket; +import org.checkerframework.checker.mustcall.qual.*; + +class InstanceInitializer { + // :: error: required.method.not.called + private @Owning Socket s; + + private final int DEFAULT_PORT = 5; + private final String DEFAULT_ADDR = "localhost"; + + { + try { + // :: error: required.method.not.called + s = new Socket(DEFAULT_ADDR, DEFAULT_PORT); + } catch (Exception e) { + } + } + + { + try { + // :: error: required.method.not.called + s = new Socket(DEFAULT_ADDR, DEFAULT_PORT); + } catch (Exception e) { + } + } + + { + try { + // :: error: required.method.not.called + Socket s1 = new Socket(DEFAULT_ADDR, DEFAULT_PORT); + } catch (Exception e) { + } + } + + { + Socket s1 = null; + try { + s1 = new Socket(DEFAULT_ADDR, DEFAULT_PORT); + } catch (Exception e) { + } + s1.close(); + } + + public InstanceInitializer() throws Exception { + // :: error: required.method.not.called + s = new Socket(DEFAULT_ADDR, DEFAULT_PORT); + } +} diff --git a/checker/tests/resourceleak/Issue4815.java b/checker/tests/resourceleak/Issue4815.java new file mode 100644 index 0000000000..0a2f9b5275 --- /dev/null +++ b/checker/tests/resourceleak/Issue4815.java @@ -0,0 +1,19 @@ +// Test case for https://tinyurl.com/cfissue/4815 + +import java.util.List; +import org.checkerframework.checker.mustcall.qual.MustCall; +import org.checkerframework.checker.mustcall.qual.Owning; + +public class Issue4815 { + public void initialize( + // This error is a false positive, so if the checker stops finding it that would be fine. + // :: error: (required.method.not.called) + List list, @Owning @MustCall("initialize") T object) { + object.initialize(); + list.add(object); + } + + private static class Component { + void initialize() {} + } +} diff --git a/checker/tests/resourceleak/MultipleMethodParamsMustCallAliasTest.java b/checker/tests/resourceleak/MultipleMethodParamsMustCallAliasTest.java new file mode 100644 index 0000000000..153a10698d --- /dev/null +++ b/checker/tests/resourceleak/MultipleMethodParamsMustCallAliasTest.java @@ -0,0 +1,88 @@ +import java.io.*; +import java.net.*; +import org.checkerframework.checker.calledmethods.qual.*; +import org.checkerframework.checker.mustcall.qual.*; +import org.checkerframework.common.returnsreceiver.qual.*; + +class MultipleMethodParamsMustCallAliasTest { + + void testMultiMethodParamsCorrect1(@Owning InputStream in1, @Owning InputStream in2) + throws IOException { + + ReplicaInputStreams r = new ReplicaInputStreams(in1, in2); + + r.close(); + } + + void testMultiMethodParamsCorrect2(@Owning InputStream in1, @Owning InputStream in2) + throws IOException { + + ReplicaInputStreams r = new ReplicaInputStreams(in1, in2); + + try { + in1.close(); + } catch (IOException e) { + } finally { + in2.close(); + } + } + + void testMultiMethodParamsCorrect3(@Owning InputStream in1, @Owning InputStream in2) + throws IOException { + + ReplicaInputStreams r = new ReplicaInputStreams(in1, in2); + + try { + in1.close(); + } finally { + in2.close(); + } + } + + // :: error: required.method.not.called + void testMultiMethodParamsWrong1(@Owning InputStream in1, @Owning InputStream in2) + throws IOException { + + ReplicaInputStreams r = new ReplicaInputStreams(in1, in2); + + in1.close(); + } + + // :: error: required.method.not.called + void testMultiMethodParamsWrong2(@Owning InputStream in1, @Owning InputStream in2) + throws IOException { + + ReplicaInputStreams r = new ReplicaInputStreams(in1, in2); + + in2.close(); + } + + // :: error: required.method.not.called + void testMultiMethodParamsWrong3(@Owning InputStream in1) throws IOException { + // :: error: required.method.not.called + Socket socket = new Socket("address", 12); + ReplicaInputStreams r = new ReplicaInputStreams(in1, socket.getInputStream()); + } + + class ReplicaInputStreams implements Closeable { + + private final @Owning InputStream in1; + private final @Owning InputStream in2; + + public @MustCallAlias ReplicaInputStreams( + @MustCallAlias InputStream i1, @MustCallAlias InputStream i2) { + this.in1 = i1; + this.in2 = i2; + } + + @Override + @EnsuresCalledMethods( + value = {"this.in1", "this.in2"}, + methods = {"close"}) + // :: error: destructor.exceptional.postcondition + public void close() throws IOException { + in1.close(); + in2.close(); + } + } +} diff --git a/checker/tests/resourceleak/MustCallAliasImplWrong1.java b/checker/tests/resourceleak/MustCallAliasImplWrong1.java index 8cb88dd09e..912cc7b0b7 100644 --- a/checker/tests/resourceleak/MustCallAliasImplWrong1.java +++ b/checker/tests/resourceleak/MustCallAliasImplWrong1.java @@ -10,7 +10,7 @@ public class MustCallAliasImplWrong1 implements Closeable { final @Owning Closeable foo; - // :: error: required.method.not.called + // :: error: mustcallalias.out.of.scope public @MustCallAlias MustCallAliasImplWrong1(@MustCallAlias Closeable foo) { this.foo = null; } diff --git a/checker/tests/resourceleak/MustCallAliasImplWrong2.java b/checker/tests/resourceleak/MustCallAliasImplWrong2.java index a302fbfa43..27a4453dcb 100644 --- a/checker/tests/resourceleak/MustCallAliasImplWrong2.java +++ b/checker/tests/resourceleak/MustCallAliasImplWrong2.java @@ -10,7 +10,7 @@ public class MustCallAliasImplWrong2 implements Closeable { final /*@Owning*/ Closeable foo; - // :: error: required.method.not.called + // :: error: mustcallalias.out.of.scope public @MustCallAlias MustCallAliasImplWrong2(@MustCallAlias Closeable foo) { this.foo = foo; } diff --git a/checker/tests/resourceleak/MustCallAliasPassthroughChain.java b/checker/tests/resourceleak/MustCallAliasPassthroughChain.java index 099e7abba9..c75c3d05a8 100644 --- a/checker/tests/resourceleak/MustCallAliasPassthroughChain.java +++ b/checker/tests/resourceleak/MustCallAliasPassthroughChain.java @@ -34,19 +34,19 @@ class MustCallAliasPassthroughChain { return s; } - // :: error: required.method.not.called + // :: error: mustcallalias.out.of.scope static @MustCallAlias InputStream chain_bad1(@MustCallAlias InputStream is) { InputStream s = withMCA(chain1(is)); return null; } - // :: error: required.method.not.called + // :: error: mustcallalias.out.of.scope static @MustCallAlias InputStream chain_bad2(@MustCallAlias InputStream is) { withMCA(chain1(is)); return null; } - // :: error: required.method.not.called + // :: error: mustcallalias.out.of.scope static @MustCallAlias InputStream chain_bad3(@MustCallAlias InputStream is, boolean b) { return b ? null : withMCA(chain1(is)); } diff --git a/checker/tests/resourceleak/MustCallAliasPassthroughWrong1.java b/checker/tests/resourceleak/MustCallAliasPassthroughWrong1.java index 8d75da2923..c3a4a05f11 100644 --- a/checker/tests/resourceleak/MustCallAliasPassthroughWrong1.java +++ b/checker/tests/resourceleak/MustCallAliasPassthroughWrong1.java @@ -7,7 +7,7 @@ import org.checkerframework.checker.mustcall.qual.*; class MustCallAliasPassthroughWrong1 extends FilterInputStream { - // :: error: required.method.not.called + // :: error: mustcallalias.out.of.scope @MustCallAlias MustCallAliasPassthroughWrong1(@MustCallAlias InputStream is) { super(null); } diff --git a/checker/tests/resourceleak/MustCallAliasPassthroughWrong2.java b/checker/tests/resourceleak/MustCallAliasPassthroughWrong2.java index 19a24e9c98..6607ee942d 100644 --- a/checker/tests/resourceleak/MustCallAliasPassthroughWrong2.java +++ b/checker/tests/resourceleak/MustCallAliasPassthroughWrong2.java @@ -1,7 +1,7 @@ // A test that a class can extend another class with an MCA constructor, // and have its own constructor be MCA as well. // This version passes the MCA param to another method instead of the passthrough constructor. -// This is actually okay - the stream does get closed, if it needs to be closed - though the +// This is sort of okay - the stream does get closed, if it needs to be closed - though the // MCA annotation on the return type is super misleading and will lead to FPs. It would be better // to annotate code like this with @Owning on the constructor. @@ -10,6 +10,7 @@ import org.checkerframework.checker.mustcall.qual.*; class MustCallAliasPassthroughWrong2 extends FilterInputStream { + // :: error: mustcallalias.out.of.scope @MustCallAlias MustCallAliasPassthroughWrong2(@MustCallAlias InputStream is) throws Exception { super(null); closeIS(is); diff --git a/checker/tests/resourceleak/MustCallAliasPassthroughWrong4.java b/checker/tests/resourceleak/MustCallAliasPassthroughWrong4.java index af2d060454..08f0b882eb 100644 --- a/checker/tests/resourceleak/MustCallAliasPassthroughWrong4.java +++ b/checker/tests/resourceleak/MustCallAliasPassthroughWrong4.java @@ -1,15 +1,15 @@ // A test that a class can extend another class with an MCA constructor, // and have its own constructor be MCA as well. // This version just closes the MCA parameter, which isn't wrong so much as weird but I wanted a -// test for it. +// test for it. Issuing an error here is appropriate, because no aliasing relationship +// actually exists after the constructor returns. import java.io.*; import org.checkerframework.checker.calledmethods.qual.*; import org.checkerframework.checker.mustcall.qual.*; class MustCallAliasPassthroughWrong4 extends FilterInputStream { - // I mean I guess this return type is technically okay - it's too conservative (@Owning on the - // param would be better) but I see no reason not to verify it. + // :: error: mustcallalias.out.of.scope @MustCallAlias MustCallAliasPassthroughWrong4(@MustCallAlias InputStream is) throws Exception { super(null); is.close(); diff --git a/checker/tests/resourceleak/MustCallAliasSubstitution.java b/checker/tests/resourceleak/MustCallAliasSubstitution.java new file mode 100644 index 0000000000..5e27e18933 --- /dev/null +++ b/checker/tests/resourceleak/MustCallAliasSubstitution.java @@ -0,0 +1,25 @@ +// A test case that checks that resolving the obligations of one object and then +// substituting a fresh object is not counted as an alias, for the purpose of MustCallAlias +// verification. + +import java.io.*; +import java.net.Socket; +import org.checkerframework.checker.mustcall.qual.*; + +class MustCallAliasSubstitution { + + // :: error: mustcallalias.out.of.scope + static @MustCallAlias Closeable example(@MustCallAlias Closeable p) throws IOException { + p.close(); + return new Socket("localhost", 5000); + } + + // This method demonstrates how a false negative could occur, if no error was issued + // on example(). + void use(Closeable c) throws IOException { + // s never gets closed, but the checker permits this code, because it believes + // that s and c are aliased. + Closeable s = example(c); + c.close(); + } +} diff --git a/checker/tests/resourceleak/ReplicaInputStreams.java b/checker/tests/resourceleak/ReplicaInputStreams.java new file mode 100644 index 0000000000..9e3656b582 --- /dev/null +++ b/checker/tests/resourceleak/ReplicaInputStreams.java @@ -0,0 +1,28 @@ +// A test case for https://github.com/typetools/checker-framework/issues/4838. + +import java.io.Closeable; +import java.io.IOException; +import java.io.InputStream; +import org.checkerframework.checker.calledmethods.qual.EnsuresCalledMethods; +import org.checkerframework.checker.mustcall.qual.Owning; + +class ReplicaInputStreams implements Closeable { + + private final @Owning InputStream in1; + private final @Owning InputStream in2; + + public ReplicaInputStreams(@Owning InputStream i1, @Owning InputStream i2) { + this.in1 = i1; + this.in2 = i2; + } + + @Override + @EnsuresCalledMethods( + value = {"this.in1", "this.in2"}, + methods = {"close"}) + // :: error: destructor.exceptional.postcondition + public void close() throws IOException { + in1.close(); + in2.close(); + } +} diff --git a/checker/tests/resourceleak/ReplicaInputStreams2.java b/checker/tests/resourceleak/ReplicaInputStreams2.java new file mode 100644 index 0000000000..c5df98e1ba --- /dev/null +++ b/checker/tests/resourceleak/ReplicaInputStreams2.java @@ -0,0 +1,31 @@ +// A test case for https://github.com/typetools/checker-framework/issues/4838. +// This variant uses a try-finally in the destructor, so it is correct. + +import java.io.Closeable; +import java.io.IOException; +import java.io.InputStream; +import org.checkerframework.checker.calledmethods.qual.EnsuresCalledMethods; +import org.checkerframework.checker.mustcall.qual.Owning; + +class ReplicaInputStreams2 implements Closeable { + + private final @Owning InputStream in1; + private final @Owning InputStream in2; + + public ReplicaInputStreams2(@Owning InputStream i1, @Owning InputStream i2) { + this.in1 = i1; + this.in2 = i2; + } + + @Override + @EnsuresCalledMethods( + value = {"this.in1", "this.in2"}, + methods = {"close"}) + public void close() throws IOException { + try { + in1.close(); + } finally { + in2.close(); + } + } +} diff --git a/checker/tests/resourceleak/TernaryExpressions.java b/checker/tests/resourceleak/TernaryExpressions.java new file mode 100644 index 0000000000..03e202e6ad --- /dev/null +++ b/checker/tests/resourceleak/TernaryExpressions.java @@ -0,0 +1,118 @@ +import org.checkerframework.checker.calledmethods.qual.*; +import org.checkerframework.checker.mustcall.qual.*; +import org.checkerframework.common.returnsreceiver.qual.*; + +class TernaryExpressions { + + @MustCall("a") class Foo { + void a() {} + + @This Foo b() { + return this; + } + + void c(@CalledMethods("a") Foo this) {} + } + + Foo makeFoo() { + return new Foo(); + } + + static void takeOwnership(@Owning Foo foo) { + foo.a(); + } + + /** cases where ternary expressions are assigned to a variable */ + void testTernaryAssigned(boolean b) { + Foo ternary1 = b ? new Foo() : makeFoo(); + ternary1.a(); + + // :: error: required.method.not.called + Foo ternary2 = b ? new Foo() : makeFoo(); + + // :: error: required.method.not.called + Foo x = new Foo(); + Foo ternary3 = b ? new Foo() : x; + ternary3.a(); + + Foo y = new Foo(); + Foo ternary4 = b ? y : y; + ternary4.a(); + + takeOwnership(b ? new Foo() : makeFoo()); + + // :: error: required.method.not.called + Foo x2 = new Foo(); + takeOwnership(b ? x2 : null); + + int i = 10; + Foo ternaryInLoop = null; + while (i > 0) { + // :: error: required.method.not.called + ternaryInLoop = b ? null : new Foo(); + i--; + } + ternaryInLoop.a(); + + (b ? new Foo() : makeFoo()).a(); + } + + /** + * tests where ternary and cast expressions (possibly nested) may or may not be assigned to a + * variable + */ + void testTernaryCastUnassigned(boolean b) { + // :: error: required.method.not.called + if ((b ? new Foo() : null) != null) { + b = !b; + } + + // :: error: required.method.not.called + if ((b ? makeFoo() : null) != null) { + b = !b; + } + + Foo x = new Foo(); + if ((b ? x : null) != null) { + b = !b; + } + x.a(); + + // :: error: required.method.not.called + if (((Foo) new Foo()) != null) { + b = !b; + } + + // double cast; no error + Foo doubleCast = (Foo) ((Foo) makeFoo()); + doubleCast.a(); + + // nesting casts and ternary expressions; no error + Foo deepNesting = (b ? (!b ? makeFoo() : (Foo) makeFoo()) : ((Foo) new Foo())); + deepNesting.a(); + } + + @Owning + Foo testTernaryReturnOk(boolean b) { + return b ? new Foo() : makeFoo(); + } + + @Owning + Foo testTernaryReturnBad(boolean b) { + // :: error: required.method.not.called + Foo x = new Foo(); + return b ? x : makeFoo(); + } + + @MustCall("toString") static class Sub1 extends Object {} + + @MustCall("clone") static class Sub2 extends Object {} + + static void testTernarySubtyping(boolean b) { + // :: error: required.method.not.called + Object toStringAndClone = b ? new Sub1() : new Sub2(); + // at this point, for soundness, we should be responsible for calling both toString and clone on + // obj... + toStringAndClone.toString(); + } +} diff --git a/checker/tests/resourceleak/TryWithResourcesFP.java b/checker/tests/resourceleak/TryWithResourcesFP.java new file mode 100644 index 0000000000..78ac9a8558 --- /dev/null +++ b/checker/tests/resourceleak/TryWithResourcesFP.java @@ -0,0 +1,27 @@ +// Based on a false positive reported on the BibTeX project + +import java.io.File; +import java.io.IOException; +import java.io.PrintWriter; +import org.plumelib.util.EntryReader; +import org.plumelib.util.UtilPlume; + +@SuppressWarnings("deprecation") +public final class TryWithResourcesFP { + public static void main(String[] args) { + for (String filename : args) { + File inFile = new File(filename); + File outFile = new File(inFile.getName()); // in current directory + // Delete the file to work around a bug. Files.newBufferedWriter (which is called by + // UtilPlume.bufferedFileWriter) seems to have a bug where it does not correctly truncate the + // file first. If the target file already exists, then characters beyond what is written + // remain in the file. + outFile.delete(); + try (PrintWriter out = new PrintWriter(UtilPlume.bufferedFileWriter(outFile.toString())); + EntryReader er = new EntryReader(filename)) { + } catch (IOException e) { + + } + } + } +} diff --git a/checker/tests/resourceleak/TryWithResourcesMultiResources.java b/checker/tests/resourceleak/TryWithResourcesMultiResources.java new file mode 100644 index 0000000000..c2ada57e2f --- /dev/null +++ b/checker/tests/resourceleak/TryWithResourcesMultiResources.java @@ -0,0 +1,42 @@ +import java.io.IOException; +import java.net.Socket; +import org.checkerframework.checker.calledmethods.qual.*; +import org.checkerframework.checker.mustcall.qual.*; + +public class TryWithResourcesMultiResources { + + class OuterResource implements java.io.Closeable { + private final @Owning Socket socket; + + public @MustCallAlias OuterResource(@MustCallAlias Socket sock) throws IOException { + this.socket = sock; + } + + @Override + @EnsuresCalledMethods( + value = {"this.socket"}, + methods = {"close"}) + public void close() throws IOException { + this.socket.close(); + } + } + + // If "new OuterResource" throws an exception, then the socket won't be released. + public void multiResourcesWrong(String address, int port) { + // :: error: required.method.not.called + try (OuterResource outer = new OuterResource(new Socket(address, port))) { + + } catch (Exception e) { + + } + } + + public void multiResourcesCorrect(String address, int port) { + try (Socket s = new Socket(address, port); + OuterResource outer = new OuterResource(s)) { + + } catch (Exception e) { + + } + } +} diff --git a/checker/tests/resourceleak/TwoResourcesECM.java b/checker/tests/resourceleak/TwoResourcesECM.java new file mode 100644 index 0000000000..dad157ee1a --- /dev/null +++ b/checker/tests/resourceleak/TwoResourcesECM.java @@ -0,0 +1,35 @@ +// A test case for https://github.com/typetools/checker-framework/issues/4838. +// +// This test that shows that no unsoundess occurs when a single close() method is responsible +// for closing two resources. + +import java.io.IOException; +import java.net.Socket; +import org.checkerframework.checker.calledmethods.qual.*; +import org.checkerframework.checker.mustcall.qual.*; + +@MustCall("dispose") class TwoResourcesECM { + @Owning Socket s1, s2; + + // The contracts.postcondition error below is thrown because s1 is not final, + // and therefore might theoretically be side-effected by the call to s2.close() + // even on the non-exceptional path. See ReplicaInputStreams.java for a variant + // of this test where such an error is not issued. Because this method can leak + // along both regular and exceptional exits, both errors are issued. + @EnsuresCalledMethods( + value = {"this.s1", "this.s2"}, + methods = {"close"}) + // :: error: contracts.postcondition :: error: destructor.exceptional.postcondition + public void dispose() throws IOException { + s1.close(); + s2.close(); + } + + static void test1(TwoResourcesECM obj) { + try { + obj.dispose(); + } catch (IOException ioe) { + + } + } +} diff --git a/checker/tests/stubparser-records/PairRecord.astub b/checker/tests/stubparser-records/PairRecord.astub new file mode 100644 index 0000000000..0c9b188359 --- /dev/null +++ b/checker/tests/stubparser-records/PairRecord.astub @@ -0,0 +1,5 @@ +import org.checkerframework.checker.nullness.qual.Nullable; + +record PairRecord(String key, @Nullable Object value) { + PairRecord(@Nullable String val); +} diff --git a/checker/tests/stubparser-records/PairRecord.java b/checker/tests/stubparser-records/PairRecord.java new file mode 100644 index 0000000000..eb7fbf6638 --- /dev/null +++ b/checker/tests/stubparser-records/PairRecord.java @@ -0,0 +1,6 @@ +record PairRecord(String key, Object value) { + + PairRecord(String val) { + this("", val); + } +} diff --git a/checker/tests/stubparser-records/RecordStubbed.astub b/checker/tests/stubparser-records/RecordStubbed.astub new file mode 100644 index 0000000000..ca7fbbd968 --- /dev/null +++ b/checker/tests/stubparser-records/RecordStubbed.astub @@ -0,0 +1,14 @@ +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; + +/** + * nxx is nullable in the record but non-null in constructor and accessor via stubs + * nsxx is nullable in the stubs but non-null in constructor and accessor via stubs + * xnn is de-facto non-null in record but nullable in constructor and accessor via stubs + */ +public record RecordStubbed(String nxx, @Nullable String nsxx, Integer xnn) { + RecordStubbed(@NonNull String nxx, @NonNull String nsxx, @Nullable Integer xnn); + @NonNull String nxx(); + @NonNull String nsxx(); + @Nullable Integer xnn(); +} diff --git a/checker/tests/stubparser-records/RecordStubbed.java b/checker/tests/stubparser-records/RecordStubbed.java new file mode 100644 index 0000000000..fc48281915 --- /dev/null +++ b/checker/tests/stubparser-records/RecordStubbed.java @@ -0,0 +1,12 @@ +import org.checkerframework.checker.nullness.qual.Nullable; + +/** + * nxx is nullable in the record but non-null in constructor and accessor via stubs nsxx is nullable + * in the stubs but non-null in constructor and accessor via stubs xnn is de-facto non-null in + * record but nullable in constructor and accessor via stubs + */ +public record RecordStubbed(@Nullable String nxx, String nsxx, Integer xnn) { + RecordStubbed(Integer a, String b, String c) { + this(c, b, a); + } +} diff --git a/checker/tests/stubparser-records/RecordUsage.java b/checker/tests/stubparser-records/RecordUsage.java new file mode 100644 index 0000000000..adbafe2e00 --- /dev/null +++ b/checker/tests/stubparser-records/RecordUsage.java @@ -0,0 +1,24 @@ +import org.checkerframework.checker.nullness.qual.NonNull; + +class PairUsage { + public void makePairs() { + PairRecord a = new PairRecord("key", "value"); + PairRecord b = new PairRecord(null); + // :: error: (assignment) + @NonNull Object o = a.value(); + PairRecord p = new PairRecord("key", null); + } + + public void makeStubbed() { + RecordStubbed r = new RecordStubbed("a", "b", 7); + RecordStubbed r1 = new RecordStubbed("a", "b", null); + // :: error: (argument) + RecordStubbed r2 = new RecordStubbed((String) null, "b", null); + // :: error: (argument) + RecordStubbed r3 = new RecordStubbed("a", null, null); + @NonNull Object o = r.nxx(); + @NonNull Object o2 = r.nsxx(); + // :: error: (assignment) + @NonNull Object o3 = r.xnn(); + } +} diff --git a/checker/tests/tainting/CaptureSubtype.java b/checker/tests/tainting/CaptureSubtype.java new file mode 100644 index 0000000000..10c729097c --- /dev/null +++ b/checker/tests/tainting/CaptureSubtype.java @@ -0,0 +1,16 @@ +import org.checkerframework.checker.tainting.qual.Tainted; +import org.checkerframework.checker.tainting.qual.Untainted; + +public class CaptureSubtype { + + class MyGeneric {} + + class SubGeneric extends MyGeneric {} + + class UseMyGeneric { + SubGeneric wildcardUnbounded = new SubGeneric<@Untainted Number>(); + + MyGeneric wildcardOutsideUB = wildcardUnbounded; + MyGeneric wildcardInsideUB2 = wildcardUnbounded; + } +} diff --git a/checker/tests/tainting/Issue1111.java b/checker/tests/tainting/Issue1111.java index 3135870fbc..8e924b2d50 100644 --- a/checker/tests/tainting/Issue1111.java +++ b/checker/tests/tainting/Issue1111.java @@ -7,6 +7,7 @@ public class Issue1111 { void foo(Box box, List list) { + // :: error: (argument) bar(box, list); } diff --git a/checker/tests/tainting/SameTypeBounds.java b/checker/tests/tainting/SameTypeBounds.java new file mode 100644 index 0000000000..40adb2a69b --- /dev/null +++ b/checker/tests/tainting/SameTypeBounds.java @@ -0,0 +1,45 @@ +package wildcards; + +import org.checkerframework.checker.tainting.qual.Tainted; +import org.checkerframework.checker.tainting.qual.Untainted; + +public class SameTypeBounds { + static class MyGen {} + + void test1(MyGen p) { + // TODO: error: invalid type + // The upper and lower bound must have the same annotation because the bounds are collasped + // during capture conversion. + MyGen o = p; + // :: error: (assignment) + p = o; + } + + void test2(MyGen p) { + // :: error: (assignment) + MyGen<@Untainted ? super @Untainted Object> o = p; + // :: error: (assignment) + p = o; + } + + void test3(MyGen<@Untainted Object> p) { + // :: error: (assignment) + MyGen o = p; + // :: error: (assignment) + p = o; + } + + static class MyClass {} + + static class MySubClass extends MyClass {} + + class Gen {} + + void test3(Gen p, Gen p2) { + // TODO: error: invalid type + Gen o = p; + o = p2; + // :: error: (assignment) + p = p2; + } +} diff --git a/checker/tests/tainting/WildcardArrayBound.java b/checker/tests/tainting/WildcardArrayBound.java new file mode 100644 index 0000000000..7be3bb3bd9 --- /dev/null +++ b/checker/tests/tainting/WildcardArrayBound.java @@ -0,0 +1,39 @@ +package wildcards; + +import java.io.Serializable; +import org.checkerframework.checker.tainting.qual.Tainted; +import org.checkerframework.checker.tainting.qual.Untainted; + +public class WildcardArrayBound { + interface MyInterface {} + + abstract static class Other { + void use1( + Other x, + Other<@Tainted MyInterface @Untainted []> y) { + Other z = y; + // :: error: (assignment) + x = y; + } + + void use( + Other x, + Other<@Tainted MyInterface @Untainted []> y) { + // :: error: (assignment) + x = y; + @Untainted Serializable s = x.getU(); + @Untainted MyInterface @Untainted [] sw = x.getU(); + } + + abstract U getU(); + } + + abstract static class Another { + void use(Another x) { + Serializable s = x.getU(); + MyInterface[] sw = x.getU(); + } + + abstract U getU(); + } +} diff --git a/checker/tests/tainting/WildcardMethodArgument.java b/checker/tests/tainting/WildcardMethodArgument.java new file mode 100644 index 0000000000..d9ad256d29 --- /dev/null +++ b/checker/tests/tainting/WildcardMethodArgument.java @@ -0,0 +1,13 @@ +import com.sun.source.tree.ExpressionTree; +import java.util.ArrayList; +import java.util.List; + +public class WildcardMethodArgument { + abstract static class MyClass { + abstract List getArguments(); + } + + void method(MyClass myClass) { + List javacArgs = new ArrayList<>(myClass.getArguments()); + } +} diff --git a/checker/tests/wpi-many/README.md b/checker/tests/wpi-many/README.md index 45d55e0ac5..fadd9b8594 100644 --- a/checker/tests/wpi-many/README.md +++ b/checker/tests/wpi-many/README.md @@ -6,6 +6,7 @@ The projects listed in `testin.txt` are derived from plume-lib projects; each is These forks have had their (inferrable) annotations removed, and their typical checker build infrastructure disabled. The `./gradlew wpiManyTest` task defined in `checker/build.gradle` runs the `wpi-many.sh` script on these projects, and then checks that they typecheck afterwards. +The use of a hard fork means these tests may fail to compile under newer versions of the JDK. To add a new project (named `$PROJECT` below) to `testin.txt`, follow these steps: 1. Create a new GitHub repository under your own user name with the name "wpi-many-tests-$PROJECT". diff --git a/checker/tests/wpi-many/testin.txt b/checker/tests/wpi-many/testin.txt index 28949b3855..f5fdbbb635 100644 --- a/checker/tests/wpi-many/testin.txt +++ b/checker/tests/wpi-many/testin.txt @@ -1,5 +1,7 @@ -https://github.com/kelloggm/wpi-many-tests-bcel-util f15bc9cd3f6f62bbea459663be49137a64797611 -https://github.com/kelloggm/wpi-many-tests-bibtex-clean 409461c024a3b73d68af5eb7b5861cc047794eff -https://github.com/kelloggm/wpi-many-tests-html-pretty-print c1c12d7f296061ef4c8a180c2832b29a5461e5c5 -https://github.com/kelloggm/-wpi-many-tests-bibtex-clean c24239d895236a88aa3bdc1459b2d2253723233c -https://github.com/kelloggm/wpi-many-tests-ensures-called-methods 666c554aa6c4165a5189f3fdc60486fb6cbac695 +https://github.com/kelloggm/wpi-many-tests-bcel-util 9a7706607425e6fbbd2c3915364eef020c39a2f9 +https://github.com/kelloggm/wpi-many-tests-bibtex-clean 703c0190ab7d7436a5be6e160667f194da44fdee +https://github.com/kelloggm/wpi-many-tests-html-pretty-print 37e5f4fa0506690c968ec6f3e316c1d0a111bded +https://github.com/kelloggm/-wpi-many-tests-bibtex-clean bd6c1cff1ba3fb8a6ecbbb73561ec2a43d018a79 +# Test that the commenting feature works (if it doesn't then, this line will be read and fail, as it's not a URL). +https://github.com/kelloggm/wpi-many-tests-ensures-called-methods e1e6d4c8c5ab4190b181fcbc95b5249c3a21afae +https://github.com/Nargeshdb/wpi-many-tests-owning-field e357cb04299298425b29a58a7598378b39e43a57 diff --git a/dataflow/build.gradle b/dataflow/build.gradle index d210dfe31d..10cc296f21 100644 --- a/dataflow/build.gradle +++ b/dataflow/build.gradle @@ -7,7 +7,7 @@ dependencies { api project(':checker-qual') // Node implements org.plumelib.util.UniqueId, so this dependency must be "api". - api 'org.plumelib:plume-util:1.5.5' + api 'org.plumelib:plume-util:1.5.8' // External dependencies: // If you add an external dependency, you must shadow its packages both in the dataflow-shaded @@ -70,6 +70,18 @@ task liveVariableTest(dependsOn: [assemble, compileTestJava], group: 'Verificati if (!JavaVersion.current().java9Compatible) { jvmArgs += "-Xbootclasspath/p:${configurations.javacJar.asPath}".toString() } + else if (JavaVersion.current() > JavaVersion.VERSION_11) { + jvmArgs += [ + "--add-opens", "jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED", + "--add-opens", "jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED", + "--add-opens", "jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED", + "--add-opens", "jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED", + "--add-opens", "jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED", + "--add-opens", "jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED", + "--add-opens", "jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED" + ] + } + classpath = sourceSets.test.runtimeClasspath classpath += sourceSets.test.output mainClass = 'livevar.LiveVariable' @@ -93,6 +105,17 @@ task issue3447Test(dependsOn: [assemble, compileTestJava], group: 'Verification' if (!JavaVersion.current().java9Compatible) { jvmArgs += "-Xbootclasspath/p:${configurations.javacJar.asPath}".toString() } + else if (JavaVersion.current() > JavaVersion.VERSION_11) { + jvmArgs += [ + "--add-opens", "jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED", + "--add-opens", "jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED", + "--add-opens", "jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED", + "--add-opens", "jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED", + "--add-opens", "jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED", + "--add-opens", "jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED", + "--add-opens", "jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED" + ] + } classpath = sourceSets.test.runtimeClasspath classpath += sourceSets.test.output diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/analysis/AbstractAnalysis.java b/dataflow/src/main/java/org/checkerframework/dataflow/analysis/AbstractAnalysis.java index fa7968b9c1..871fd4836e 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/analysis/AbstractAnalysis.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/analysis/AbstractAnalysis.java @@ -11,6 +11,7 @@ import java.util.PriorityQueue; import java.util.Set; import javax.lang.model.element.Element; +import javax.lang.model.type.TypeMirror; import org.checkerframework.checker.interning.qual.FindDistinct; import org.checkerframework.checker.interning.qual.InternedDistinct; import org.checkerframework.checker.nullness.qual.EnsuresNonNull; @@ -360,6 +361,20 @@ protected final void init(ControlFlowGraph cfg) { initInitialInputs(); } + /** + * Should exceptional control flow for a particular exception type be ignored? + * + *

The default implementation always returns {@code false}. Subclasses should override the + * method to implement a different policy. + * + * @param exceptionType the exception type + * @return {@code true} if exceptional control flow due to {@code exceptionType} should be + * ignored, {@code false} otherwise + */ + protected boolean isIgnoredExceptionType(TypeMirror exceptionType) { + return false; + } + /** * Initialize fields of this object based on a given control flow graph. Sub-class may override * this method to initialize customized fields. diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/analysis/BackwardAnalysisImpl.java b/dataflow/src/main/java/org/checkerframework/dataflow/analysis/BackwardAnalysisImpl.java index f7a6c192a6..99bc52f874 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/analysis/BackwardAnalysisImpl.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/analysis/BackwardAnalysisImpl.java @@ -4,6 +4,8 @@ import java.util.List; import java.util.ListIterator; import java.util.Map; +import java.util.Set; +import javax.lang.model.type.TypeMirror; import org.checkerframework.checker.interning.qual.FindDistinct; import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.checker.nullness.qual.RequiresNonNull; @@ -253,25 +255,23 @@ protected void propagateStoresTo( protected void addStoreAfter(Block pred, @Nullable Node node, S s, boolean addBlockToWorklist) { // If the block pred is an exception block, decide whether the block of passing node is an // exceptional successor of the block pred - if (pred instanceof ExceptionBlock - && ((ExceptionBlock) pred).getSuccessor() != null - && node != null) { - @Nullable Block succBlock = ((ExceptionBlock) pred).getSuccessor(); - @Nullable Block block = node.getBlock(); - if (succBlock != null && block != null && succBlock.getUid() == block.getUid()) { - // If the block of passing node is an exceptional successor of Block pred, propagate - // store to the exceptionStores. Currently it doesn't track the label of an - // exceptional edge from exception block to its exceptional successors in backward - // direction. Instead, all exception stores of exceptional successors of an - // exception block will merge to one exception store at the exception block - ExceptionBlock ebPred = (ExceptionBlock) pred; - S exceptionStore = exceptionStores.get(ebPred); - S newExceptionStore = (exceptionStore != null) ? exceptionStore.leastUpperBound(s) : s; - if (!newExceptionStore.equals(exceptionStore)) { - exceptionStores.put(ebPred, newExceptionStore); - inputs.put(ebPred, new TransferInput(node, this, newExceptionStore)); - addBlockToWorklist = true; - } + TypeMirror excSuccType = getSuccExceptionType(pred, node); + if (excSuccType != null) { + if (isIgnoredExceptionType(excSuccType)) { + return; + } + // If the block of passing node is an exceptional successor of Block pred, propagate + // store to the exceptionStores. Currently it doesn't track the label of an + // exceptional edge from exception block to its exceptional successors in backward + // direction. Instead, all exception stores of exceptional successors of an + // exception block will merge to one exception store at the exception block + ExceptionBlock ebPred = (ExceptionBlock) pred; + S exceptionStore = exceptionStores.get(ebPred); + S newExceptionStore = (exceptionStore != null) ? exceptionStore.leastUpperBound(s) : s; + if (!newExceptionStore.equals(exceptionStore)) { + exceptionStores.put(ebPred, newExceptionStore); + inputs.put(ebPred, new TransferInput(node, this, newExceptionStore)); + addBlockToWorklist = true; } } else { S predOutStore = getStoreAfter(pred); @@ -287,6 +287,33 @@ protected void addStoreAfter(Block pred, @Nullable Node node, S s, boolean addBl } } + /** + * Checks if the block for a node is an exceptional successor of a predecessor block, and if so, + * returns the exception type for the control-flow edge. + * + * @param pred the predecessor block + * @param node the successor node + * @return the exception type leading to a control flow edge from {@code pred} to the block for + * {@code node}, if it exists; {@code null} otherwise + */ + private @Nullable TypeMirror getSuccExceptionType(Block pred, @Nullable Node node) { + if (pred instanceof ExceptionBlock && node != null) { + Block block = node.getBlock(); + if (block != null) { + Map> exceptionalSuccessors = + ((ExceptionBlock) pred).getExceptionalSuccessors(); + for (TypeMirror excType : exceptionalSuccessors.keySet()) { + for (Block excSuccBlock : exceptionalSuccessors.get(excType)) { + if (excSuccBlock.getUid() == block.getUid()) { + return excType; + } + } + } + } + } + return null; + } + /** * Returns the store corresponding to the location right after the basic block {@code b}. * diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/analysis/ConditionalTransferResult.java b/dataflow/src/main/java/org/checkerframework/dataflow/analysis/ConditionalTransferResult.java index 6e46afe6bc..b133cfb265 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/analysis/ConditionalTransferResult.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/analysis/ConditionalTransferResult.java @@ -11,8 +11,7 @@ * contains information valid when the previous boolean-valued expression was true, and the 'else' * store contains information valid when the expression was false. * - *

The result of {@code getRegularStore} will be the least upper bound of the two underlying - * stores. + *

{@link getRegularStore} returns the least upper bound of the two underlying stores. * * @param type of the abstract value that is tracked * @param the store type used in the analysis diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/analysis/ForwardAnalysisImpl.java b/dataflow/src/main/java/org/checkerframework/dataflow/analysis/ForwardAnalysisImpl.java index c2412c69c6..a66e835942 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/analysis/ForwardAnalysisImpl.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/analysis/ForwardAnalysisImpl.java @@ -159,6 +159,9 @@ public void performAnalysisBlock(Block b) { // Propagate store to exceptional successors for (Map.Entry> e : eb.getExceptionalSuccessors().entrySet()) { TypeMirror cause = e.getKey(); + if (isIgnoredExceptionType(cause)) { + continue; + } S exceptionalStore = transferResult.getExceptionalStore(cause); if (exceptionalStore != null) { for (Block exceptionSucc : e.getValue()) { diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/block/RegularBlock.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/block/RegularBlock.java index baa74e4a33..f5207aa010 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/block/RegularBlock.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/block/RegularBlock.java @@ -1,6 +1,5 @@ package org.checkerframework.dataflow.cfg.block; -import java.util.List; import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.dataflow.cfg.node.Node; import org.checkerframework.dataflow.qual.Pure; @@ -8,16 +7,6 @@ /** A regular basic block that contains a sequence of {@link Node}s. */ public interface RegularBlock extends SingleSuccessorBlock { - /** - * Returns the unmodifiable sequence of {@link Node}s. - * - * @return the unmodifiable sequence of {@link Node}s - * @deprecated use {@link #getNodes} instead - */ - @Deprecated // 2020-08-05 - @Pure - List getContents(); - /** * Returns the regular successor block. * diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/block/RegularBlockImpl.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/block/RegularBlockImpl.java index 5e4c47a3c8..27e8c8ba08 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/block/RegularBlockImpl.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/block/RegularBlockImpl.java @@ -34,12 +34,6 @@ public void addNodes(List ts) { } } - @SuppressWarnings("deprecation") // implementation of deprecated method in interface - @Override - public List getContents() { - return getNodes(); - } - /** * {@inheritDoc} * diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/block/SingleSuccessorBlock.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/block/SingleSuccessorBlock.java index 8d383026ee..057a78b949 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/block/SingleSuccessorBlock.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/block/SingleSuccessorBlock.java @@ -4,7 +4,7 @@ import org.checkerframework.dataflow.analysis.Store.FlowRule; import org.checkerframework.dataflow.qual.Pure; -/** A basic block that has at exactly one non-exceptional successor. */ +/** A basic block that has exactly one non-exceptional successor. */ public interface SingleSuccessorBlock extends Block { /** diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/builder/CFGTranslationPhaseOne.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/builder/CFGTranslationPhaseOne.java index 6b056b0c1f..f1d318a532 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/builder/CFGTranslationPhaseOne.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/builder/CFGTranslationPhaseOne.java @@ -55,14 +55,9 @@ import com.sun.source.tree.WhileLoopTree; import com.sun.source.tree.WildcardTree; import com.sun.source.util.TreePath; -import com.sun.source.util.TreePathScanner; +import com.sun.source.util.TreeScanner; import com.sun.source.util.Trees; -import com.sun.tools.javac.code.Symbol.MethodSymbol; -import com.sun.tools.javac.code.Type; -import com.sun.tools.javac.processing.JavacProcessingEnvironment; -import com.sun.tools.javac.util.Context; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; @@ -153,6 +148,7 @@ import org.checkerframework.dataflow.cfg.node.StringConversionNode; import org.checkerframework.dataflow.cfg.node.StringLiteralNode; import org.checkerframework.dataflow.cfg.node.SuperNode; +import org.checkerframework.dataflow.cfg.node.SwitchExpressionNode; import org.checkerframework.dataflow.cfg.node.SynchronizedNode; import org.checkerframework.dataflow.cfg.node.TernaryExpressionNode; import org.checkerframework.dataflow.cfg.node.ThisNode; @@ -195,7 +191,7 @@ * (which might only be a jump). */ @SuppressWarnings("nullness") // TODO -public class CFGTranslationPhaseOne extends TreePathScanner { +public class CFGTranslationPhaseOne extends TreeScanner { /** Annotation processing environment and its associated type and tree utilities. */ final ProcessingEnvironment env; @@ -255,6 +251,9 @@ public class CFGTranslationPhaseOne extends TreePathScanner { /** Nested scopes of try-catch blocks in force at the current program point. */ private final TryStack tryStack; + /** SwitchBuilder for the current switch. Used to match yield statements to enclosing switches. */ + private SwitchBuilder switchBuilder; + /** * Maps from AST {@link Tree}s to sets of {@link Node}s. Every Tree that produces a value will * have at least one corresponding Node. Trees that undergo conversions, such as boxing or @@ -281,7 +280,7 @@ public class CFGTranslationPhaseOne extends TreePathScanner { /** * All return nodes (if any) encountered. Only includes return statements that actually return - * something + * something. */ private final List returnNodes; @@ -424,39 +423,45 @@ public CFGTranslationPhaseOne( */ public PhaseOneResult process(TreePath bodyPath, UnderlyingAST underlyingAST) { // traverse AST of the method body - Node finalNode = scan(bodyPath, null); - - // If we are building the CFG for a lambda with a single expression as the body, then - // add an extra node for the result of that lambda - if (underlyingAST.getKind() == UnderlyingAST.Kind.LAMBDA) { - LambdaExpressionTree lambdaTree = ((UnderlyingAST.CFGLambda) underlyingAST).getLambdaTree(); - if (lambdaTree.getBodyKind() == LambdaExpressionTree.BodyKind.EXPRESSION) { - Node resultNode = - new LambdaResultExpressionNode( - (ExpressionTree) lambdaTree.getBody(), finalNode, env.getTypeUtils()); - extendWithNode(resultNode); + this.path = bodyPath; + try { // "finally" clause is "this.path = null" + Node finalNode = scan(path.getLeaf(), null); + + // If we are building the CFG for a lambda with a single expression as the body, then + // add an extra node for the result of that lambda + if (underlyingAST.getKind() == UnderlyingAST.Kind.LAMBDA) { + LambdaExpressionTree lambdaTree = ((UnderlyingAST.CFGLambda) underlyingAST).getLambdaTree(); + if (lambdaTree.getBodyKind() == LambdaExpressionTree.BodyKind.EXPRESSION) { + Node resultNode = + new LambdaResultExpressionNode( + (ExpressionTree) lambdaTree.getBody(), finalNode, env.getTypeUtils()); + extendWithNode(resultNode); + } } - } - // Add marker to indicate that the next block will be the exit block. - // Note: if there is a return statement earlier in the method (which is always the case for - // non-void methods), then this is not strictly necessary. However, it is also not a problem, as - // it will just generate a degenerated control graph case that will be removed in a later phase. - nodeList.add(new UnconditionalJump(regularExitLabel)); - - return new PhaseOneResult( - underlyingAST, - treeLookupMap, - convertedTreeLookupMap, - unaryAssignNodeLookupMap, - nodeList, - bindings, - leaders, - returnNodes, - regularExitLabel, - exceptionalExitLabel, - declaredClasses, - declaredLambdas); + // Add marker to indicate that the next block will be the exit block. + // Note: if there is a return statement earlier in the method (which is always the case for + // non-void methods), then this is not strictly necessary. However, it is also not a problem, + // as it will just generate a degenerate control graph case that will be removed in a later + // phase. + nodeList.add(new UnconditionalJump(regularExitLabel)); + + return new PhaseOneResult( + underlyingAST, + treeLookupMap, + convertedTreeLookupMap, + unaryAssignNodeLookupMap, + nodeList, + bindings, + leaders, + returnNodes, + regularExitLabel, + exceptionalExitLabel, + declaredClasses, + declaredLambdas); + } finally { + this.path = null; + } } public PhaseOneResult process(CompilationUnitTree root, UnderlyingAST underlyingAST) { @@ -474,6 +479,73 @@ public PhaseOneResult process(CompilationUnitTree root, UnderlyingAST underlying */ public void handleArtificialTree(Tree tree) {} + /** + * Returns the current path for the tree currently being scanned. + * + * @return the current path + */ + public TreePath getCurrentPath() { + return path; + } + + /** Path to the tree currently being scanned. */ + private TreePath path; + + @Override + public Node scan(Tree tree, Void p) { + if (tree == null) { + return null; + } + + TreePath prev = path; + @SuppressWarnings("interning:not.interned") // Looking for exact match. + boolean treeIsLeaf = path.getLeaf() != tree; + if (treeIsLeaf) { + path = new TreePath(path, tree); + } + try { + // Must use String comparison to support compiling on JDK 11 and earlier. + // Features added between JDK 12 and JDK 17 inclusive. + switch (tree.getKind().name()) { + // case "BINDING_PATTERN": + // return visitBindingPattern17(path.getLeaf(), p); + case "SWITCH_EXPRESSION": + return visitSwitchExpression17(tree, p); + case "YIELD": + return visitYield17(tree, p); + default: + return tree.accept(this, p); + } + } finally { + path = prev; + } + } + + /** + * Visit a SwitchExpressionTree. + * + * @param yieldTree a YieldTree, typed as Tree to be backward-compatible + * @param p parameter + * @return the result of visiting the switch expression tree + */ + public Node visitYield17(Tree yieldTree, Void p) { + ExpressionTree resultExpression = TreeUtils.yieldTreeGetValue(yieldTree); + switchBuilder.buildSwitchExpressionResult(resultExpression); + return null; + } + + /** + * Visit a SwitchExpressionTree + * + * @param switchExpressionTree a SwitchExpressionTree, typed as Tree to be backward-compatible + * @param p parameter + * @return the result of visiting the switch expression tree + */ + public Node visitSwitchExpression17(Tree switchExpressionTree, Void p) { + SwitchBuilder switchBuilder = new SwitchBuilder(switchExpressionTree); + return switchBuilder.build(); + } + /* --------------------------------------------------------- */ /* Nodes and Labels Management */ /* --------------------------------------------------------- */ @@ -1279,7 +1351,7 @@ public Node visitAnnotatedType(AnnotatedTypeTree tree, Void p) { @Override public Node visitAnnotation(AnnotationTree tree, Void p) { - throw new Error("AnnotationTree is unexpected in AST to CFG translation"); + throw new BugInCF("AnnotationTree is unexpected in AST to CFG translation"); } @Override @@ -1765,7 +1837,7 @@ public Node visitCompoundAssignment(CompoundAssignmentTree tree, Void p) { targetRHS = unbox(targetLHS); value = unbox(value); } else { - throw new Error("Both argument to logical operation must be numeric or boolean"); + throw new BugInCF("Both arguments to logical operation must be numeric or boolean"); } BinaryTree operTree = @@ -1793,7 +1865,7 @@ public Node visitCompoundAssignment(CompoundAssignmentTree tree, Void p) { extendWithNode(assignNode); return assignNode; default: - throw new Error("unexpected compound assignment type"); + throw new BugInCF("unexpected compound assignment type"); } } @@ -2048,7 +2120,7 @@ public Node visitBinary(BinaryTree tree, Void p) { return node; } default: - throw new Error("unexpected binary tree: " + kind); + throw new BugInCF("unexpected binary tree: " + kind); } assert r != null : "unexpected binary tree"; extendWithNode(r); @@ -2079,6 +2151,7 @@ public Node visitBreak(BreakTree tree, Void p) { return null; } + // This visits a switch statement, not a switch expression. @Override public Node visitSwitch(SwitchTree tree, Void p) { SwitchBuilder builder = new SwitchBuilder(tree); @@ -2086,27 +2159,73 @@ public Node visitSwitch(SwitchTree tree, Void p) { return null; } - /** Helper class for handling switch statements. */ + /** + * Helper class for handling switch statements and switch expressions, including all their + * substatements such as case labels. + */ private class SwitchBuilder { - /** The switch tree. */ - private final SwitchTree switchTree; + + /** + * The tree for the switch statement or switch expression. Its type may be {@link SwitchTree} or + * {@code SwitchExpressionTree}} + */ + private final Tree switchTree; + + /** The case trees of {@code switchTree} */ + private final List caseTrees; + + /** + * The Tree for the selector expression. + * + *

+     *   switch (  selector expression ) { ... }
+     * 
+ */ + private final ExpressionTree selectorExprTree; + /** The labels for the case bodies. */ private final Label[] caseBodyLabels; - /** The Node for the switch expression. */ - private Node switchExpr; + + /** + * The Node for the assignment of the switch selector expression to a synthetic local variable. + */ + private AssignmentNode selectorExprAssignment; + + /** + * If {@link #switchTree} is a switch expression, then this is the synthetic variable tree that + * all results of {@code #switchTree} are assigned. Otherwise, this is null. + */ + private @Nullable VariableTree switchExprVarTree; /** * Construct a SwitchBuilder. * - * @param tree switch tree + * @param switchTree a {@link SwitchTree} or a {@code SwitchExpressionTree} */ - private SwitchBuilder(SwitchTree tree) { - this.switchTree = tree; - this.caseBodyLabels = new Label[switchTree.getCases().size() + 1]; + private SwitchBuilder(Tree switchTree) { + this.switchTree = switchTree; + if (switchTree instanceof SwitchTree) { + SwitchTree switchStatementTree = (SwitchTree) switchTree; + this.caseTrees = switchStatementTree.getCases(); + this.selectorExprTree = switchStatementTree.getExpression(); + } else { + this.caseTrees = TreeUtils.switchExpressionTreeGetCases(switchTree); + this.selectorExprTree = TreeUtils.switchExpressionTreeGetExpression(switchTree); + } + // "+ 1" for the default case. If the switch has an explicit default case, then + // the last element of the array is never used. + this.caseBodyLabels = new Label[caseTrees.size() + 1]; } - /** Build up the CFG for the switchTree. */ - public void build() { + /** + * Build up the CFG for the switchTree. + * + * @return if the switch is a switch expression, then a {@link SwitchExpressionNode}; otherwise, + * null + */ + public @Nullable SwitchExpressionNode build() { + SwitchBuilder oldSwitchBuilder = switchBuilder; + switchBuilder = this; TryFinallyScopeCell oldBreakTargetL = breakTargetL; breakTargetL = new TryFinallyScopeCell(new Label()); int cases = caseBodyLabels.length - 1; @@ -2115,41 +2234,24 @@ public void build() { } caseBodyLabels[cases] = breakTargetL.peekLabel(); - TypeMirror switchExprType = TreeUtils.typeOf(switchTree.getExpression()); - VariableTree variable = - treeBuilder.buildVariableDecl(switchExprType, uniqueName("switch"), findOwner(), null); - handleArtificialTree(variable); - - VariableDeclarationNode variableNode = new VariableDeclarationNode(variable); - variableNode.setInSource(false); - extendWithNode(variableNode); - - IdentifierTree variableUse = treeBuilder.buildVariableUse(variable); - handleArtificialTree(variableUse); - - LocalVariableNode variableUseNode = new LocalVariableNode(variableUse); - variableUseNode.setInSource(false); - extendWithNode(variableUseNode); + buildSelector(); - Node switchExprNode = unbox(scan(switchTree.getExpression(), null)); + buildSwitchExpressionVar(); - AssignmentTree assign = treeBuilder.buildAssignment(variableUse, switchTree.getExpression()); - handleArtificialTree(assign); - - switchExpr = new AssignmentNode(assign, variableUseNode, switchExprNode); - switchExpr.setInSource(false); - extendWithNode(switchExpr); - - extendWithNode( - new MarkerNode( - switchTree, - "start of switch statement #" + TreeUtils.treeUids.get(switchTree), - env.getTypeUtils())); + if (switchTree.getKind() == Tree.Kind.SWITCH) { + // It's a switch statement, not a switch expression. + extendWithNode( + new MarkerNode( + switchTree, + "start of switch statement #" + TreeUtils.treeUids.get(switchTree), + env.getTypeUtils())); + } + // Build CFG for the cases. Integer defaultIndex = null; for (int i = 0; i < cases; ++i) { - CaseTree caseTree = switchTree.getCases().get(i); - if (caseTree.getExpression() == null) { + CaseTree caseTree = caseTrees.get(i); + if (TreeUtils.caseTreeGetExpressions(caseTree).isEmpty()) { defaultIndex = i; } else { buildCase(caseTree, i); @@ -2159,43 +2261,179 @@ public void build() { // The checks of all cases must happen before the default case, therefore we build the // default case last. // Fallthrough is still handled correctly with the caseBodyLabels. - buildCase(switchTree.getCases().get(defaultIndex), defaultIndex); + buildCase(caseTrees.get(defaultIndex), defaultIndex); } addLabelForNextNode(breakTargetL.peekLabel()); breakTargetL = oldBreakTargetL; + if (switchTree.getKind() == Tree.Kind.SWITCH) { + // It's a switch statement, not a switch expression. + extendWithNode( + new MarkerNode( + switchTree, + "end of switch statement #" + TreeUtils.treeUids.get(switchTree), + env.getTypeUtils())); + } - extendWithNode( - new MarkerNode( - switchTree, - "end of switch statement #" + TreeUtils.treeUids.get(switchTree), - env.getTypeUtils())); + switchBuilder = oldSwitchBuilder; + if (switchTree.getKind() != Tree.Kind.SWITCH) { + // It's a switch expression, not a switch statement. + IdentifierTree switchExprVarUseTree = treeBuilder.buildVariableUse(switchExprVarTree); + handleArtificialTree(switchExprVarUseTree); + + LocalVariableNode switchExprVarUseNode = new LocalVariableNode(switchExprVarUseTree); + switchExprVarUseNode.setInSource(false); + extendWithNode(switchExprVarUseNode); + SwitchExpressionNode switchExpressionNode = + new SwitchExpressionNode( + TreeUtils.typeOf(switchTree), switchTree, switchExprVarUseNode); + extendWithNode(switchExpressionNode); + return switchExpressionNode; + } else { + return null; + } + } + + /** + * Builds the CFG for the selector expression. It also creates a synthetic variable and assigns + * the selector expression to the variable. This assignment node is stored in {@link + * #selectorExprAssignment}. It can later be used to refine the selector expression in case + * bodies. + */ + private void buildSelector() { + // Create a synthetic variable to which the switch selector expression will be assigned + TypeMirror selectorExprType = TreeUtils.typeOf(selectorExprTree); + VariableTree selectorVarTree = + treeBuilder.buildVariableDecl(selectorExprType, uniqueName("switch"), findOwner(), null); + handleArtificialTree(selectorVarTree); + + VariableDeclarationNode selectorVarNode = new VariableDeclarationNode(selectorVarTree); + selectorVarNode.setInSource(false); + extendWithNode(selectorVarNode); + + IdentifierTree selectorVarUseTree = treeBuilder.buildVariableUse(selectorVarTree); + handleArtificialTree(selectorVarUseTree); + + LocalVariableNode selectorVarUseNode = new LocalVariableNode(selectorVarUseTree); + selectorVarUseNode.setInSource(false); + extendWithNode(selectorVarUseNode); + + Node selectorExprNode = unbox(scan(selectorExprTree, null)); + + AssignmentTree assign = treeBuilder.buildAssignment(selectorVarUseTree, selectorExprTree); + handleArtificialTree(assign); + + selectorExprAssignment = new AssignmentNode(assign, selectorVarUseNode, selectorExprNode); + selectorExprAssignment.setInSource(false); + extendWithNode(selectorExprAssignment); } + /** + * If {@link #switchTree} is a switch expression tree, this method creates a synthetic variable + * whose value is the value of the switch expression. + */ + private void buildSwitchExpressionVar() { + if (switchTree.getKind() == Tree.Kind.SWITCH) { + // A switch statement does not have a value, so do nothing. + return; + } + TypeMirror switchExprType = TreeUtils.typeOf(switchTree); + switchExprVarTree = + treeBuilder.buildVariableDecl( + switchExprType, uniqueName("switchExpr"), findOwner(), null); + handleArtificialTree(switchExprVarTree); + + VariableDeclarationNode switchExprVarNode = new VariableDeclarationNode(switchExprVarTree); + switchExprVarNode.setInSource(false); + extendWithNode(switchExprVarNode); + } + + /** + * Build the CFG for the case tree, {@code tree}. + * + * @param tree a case tree whose CFG is built + * @param index the index of the case tree in {@link #caseBodyLabels} + */ private void buildCase(CaseTree tree, int index) { final Label thisBodyL = caseBodyLabels[index]; final Label nextBodyL = caseBodyLabels[index + 1]; final Label nextCaseL = new Label(); - ExpressionTree exprTree = tree.getExpression(); - if (exprTree != null) { + List exprTrees = TreeUtils.caseTreeGetExpressions(tree); + if (!exprTrees.isEmpty()) { // non-default cases - Node expr = scan(exprTree, null); - CaseNode test = new CaseNode(tree, switchExpr, expr, env.getTypeUtils()); + ArrayList exprs = new ArrayList<>(); + for (ExpressionTree exprTree : exprTrees) { + exprs.add(scan(exprTree, null)); + } + CaseNode test = new CaseNode(tree, selectorExprAssignment, exprs, env.getTypeUtils()); extendWithNode(test); extendWithExtendedNode(new ConditionalJump(thisBodyL, nextCaseL)); } addLabelForNextNode(thisBodyL); - for (StatementTree stmt : tree.getStatements()) { - scan(stmt, null); + if (tree.getStatements() != null) { + // This is a switch labeled statement groups. + for (StatementTree stmt : tree.getStatements()) { + scan(stmt, null); + } + // Handle possible fall through by adding jump to next body. + extendWithExtendedNode(new UnconditionalJump(nextBodyL)); + } else { + // This is a switch rule. + Tree bodyTree = TreeUtils.caseTreeGetBody(tree); + if (switchTree.getKind() != Tree.Kind.SWITCH && bodyTree instanceof ExpressionTree) { + buildSwitchExpressionResult((ExpressionTree) bodyTree); + } else { + scan(bodyTree, null); + // Switch rules never fall through so add jump to the break target. + assert breakTargetL != null : "no target for case statement"; + extendWithExtendedNode(new UnconditionalJump(breakTargetL.accessLabel())); + } } - extendWithExtendedNode(new UnconditionalJump(nextBodyL)); + addLabelForNextNode(nextCaseL); } + + /** + * Does the following for the result expression of a switch expression, {@code + * resultExpression}: + * + *
    + *
  1. Builds the CFG for the switch expression result. + *
  2. Creates an assignment node for the assignment of {@code resultExpression} to {@code + * switchExprVarTree}. + *
  3. Adds an unconditional jump to {@link #breakTargetL} (the end of the switch expression). + *
+ * + * @param resultExpression the result of a switch expression; either from a yield or an + * expression in a case rule + */ + void buildSwitchExpressionResult(ExpressionTree resultExpression) { + IdentifierTree switchExprVarUseTree = treeBuilder.buildVariableUse(switchExprVarTree); + handleArtificialTree(switchExprVarUseTree); + + LocalVariableNode switchExprVarUseNode = new LocalVariableNode(switchExprVarUseTree); + switchExprVarUseNode.setInSource(false); + extendWithNode(switchExprVarUseNode); + + Node resultExprNode = scan(resultExpression, null); + + AssignmentTree assign = treeBuilder.buildAssignment(switchExprVarUseTree, resultExpression); + handleArtificialTree(assign); + + AssignmentNode assignmentNode = + new AssignmentNode(assign, switchExprVarUseNode, resultExprNode); + assignmentNode.setInSource(false); + extendWithNode(assignmentNode); + // Switch rules never fall through so add jump to the break target. + assert breakTargetL != null : "no target for case statement"; + extendWithExtendedNode(new UnconditionalJump(breakTargetL.accessLabel())); + } } @Override public Node visitCase(CaseTree tree, Void p) { + // This assertion assumes that `case` appears only within a switch statement, throw new AssertionError("case visitor is implemented in SwitchBuilder"); } @@ -2223,27 +2461,85 @@ public Node visitConditionalExpression(ConditionalExpressionTree tree, Void p) { Label falseStart = new Label(); Label merge = new Label(); + // create a synthetic variable for the value of the conditional expression + VariableTree condExprVarTree = + treeBuilder.buildVariableDecl(exprType, uniqueName("condExpr"), findOwner(), null); + VariableDeclarationNode condExprVarNode = new VariableDeclarationNode(condExprVarTree); + condExprVarNode.setInSource(false); + extendWithNode(condExprVarNode); + Node condition = unbox(scan(tree.getCondition(), p)); ConditionalJump cjump = new ConditionalJump(trueStart, falseStart); extendWithExtendedNode(cjump); addLabelForNextNode(trueStart); - Node trueExpr = scan(tree.getTrueExpression(), p); - trueExpr = conditionalExprPromotion(trueExpr, exprType); + ExpressionTree trueExprTree = tree.getTrueExpression(); + Node trueExprNode = scan(trueExprTree, p); + trueExprNode = conditionalExprPromotion(trueExprNode, exprType); + + extendWithAssignmentForConditionalExpr(condExprVarTree, trueExprTree, trueExprNode); + extendWithExtendedNode(new UnconditionalJump(merge, FlowRule.BOTH_TO_THEN)); addLabelForNextNode(falseStart); - Node falseExpr = scan(tree.getFalseExpression(), p); - falseExpr = conditionalExprPromotion(falseExpr, exprType); + ExpressionTree falseExprTree = tree.getFalseExpression(); + Node falseExprNode = scan(falseExprTree, p); + falseExprNode = conditionalExprPromotion(falseExprNode, exprType); + + extendWithAssignmentForConditionalExpr(condExprVarTree, falseExprTree, falseExprNode); + extendWithExtendedNode(new UnconditionalJump(merge, FlowRule.BOTH_TO_ELSE)); addLabelForNextNode(merge); - Node node = new TernaryExpressionNode(tree, condition, trueExpr, falseExpr); + Pair treeAndLocalVarNode = + extendWithVarUseNode(condExprVarTree); + Node node = + new TernaryExpressionNode( + tree, condition, trueExprNode, falseExprNode, treeAndLocalVarNode.second); extendWithNode(node); return node; } + /** + * Extend the CFG with an assignment for either the true or false case of a conditional + * expression, assigning the value of the expression for the case to the synthetic variable for + * the conditional expression + * + * @param condExprVarTree tree for synthetic variable for conditional expression + * @param caseExprTree expression tree for the case + * @param caseExprNode node for the case + */ + private void extendWithAssignmentForConditionalExpr( + VariableTree condExprVarTree, ExpressionTree caseExprTree, Node caseExprNode) { + Pair treeAndLocalVarNode = + extendWithVarUseNode(condExprVarTree); + + AssignmentTree assign = treeBuilder.buildAssignment(treeAndLocalVarNode.first, caseExprTree); + handleArtificialTree(assign); + + AssignmentNode assignmentNode = + new AssignmentNode(assign, treeAndLocalVarNode.second, caseExprNode); + assignmentNode.setInSource(false); + extendWithNode(assignmentNode); + } + + /** + * Extend the CFG with a {@link LocalVariableNode} representing a use of some variable + * + * @param varTree tree for the variable + * @return a pair whose first element is the synthetic {@link IdentifierTree} for the use, and + * whose second element is the {@link LocalVariableNode} representing the use + */ + private Pair extendWithVarUseNode(VariableTree varTree) { + IdentifierTree condExprVarUseTree = treeBuilder.buildVariableUse(varTree); + handleArtificialTree(condExprVarUseTree); + LocalVariableNode condExprVarUseNode = new LocalVariableNode(condExprVarUseTree); + condExprVarUseNode.setInSource(false); + extendWithNode(condExprVarUseNode); + return Pair.of(condExprVarUseTree, condExprVarUseNode); + } + @Override public Node visitContinue(ContinueTree tree, Void p) { Name label = tree.getLabel(); @@ -2305,7 +2601,7 @@ public Node visitDoWhileLoop(DoWhileLoopTree tree, Void p) { @Override public Node visitErroneous(ErroneousTree tree, Void p) { - throw new Error("ErroneousTree is unexpected in AST to CFG translation"); + throw new BugInCF("ErroneousTree is unexpected in AST to CFG translation"); } @Override @@ -2755,7 +3051,7 @@ public Node visitIf(IfTree tree, Void p) { @Override public Node visitImport(ImportTree tree, Void p) { - throw new Error("ImportTree is unexpected in AST to CFG translation"); + throw new BugInCF("ImportTree is unexpected in AST to CFG translation"); } @Override @@ -2821,7 +3117,7 @@ public Node visitLiteral(LiteralTree tree, Void p) { r = new StringLiteralNode(tree); break; default: - throw new Error("unexpected literal tree"); + throw new BugInCF("unexpected literal tree"); } assert r != null : "unexpected literal tree"; extendWithNode(r); @@ -2830,12 +3126,12 @@ public Node visitLiteral(LiteralTree tree, Void p) { @Override public Node visitMethod(MethodTree tree, Void p) { - throw new Error("MethodTree is unexpected in AST to CFG translation"); + throw new BugInCF("MethodTree is unexpected in AST to CFG translation"); } @Override public Node visitModifiers(ModifiersTree tree, Void p) { - throw new Error("ModifiersTree is unexpected in AST to CFG translation"); + throw new BugInCF("ModifiersTree is unexpected in AST to CFG translation"); } @Override @@ -2928,24 +3224,7 @@ public Node visitReturn(ReturnTree tree, Void p) { ReturnNode result = null; if (ret != null) { Node node = scan(ret, p); - TreePath enclosingPath = - TreePathUtil.pathTillOfKind( - getCurrentPath(), - new HashSet<>(Arrays.asList(Tree.Kind.METHOD, Tree.Kind.LAMBDA_EXPRESSION))); - Tree enclosing = enclosingPath.getLeaf(); - if (enclosing.getKind() == Tree.Kind.LAMBDA_EXPRESSION) { - LambdaExpressionTree lambdaTree = (LambdaExpressionTree) enclosing; - Context ctx = ((JavacProcessingEnvironment) env).getContext(); - Element overriddenElement = - com.sun.tools.javac.code.Types.instance(ctx) - .findDescriptorSymbol(((Type) trees.getTypeMirror(enclosingPath)).tsym); - - result = - new ReturnNode( - tree, node, env.getTypeUtils(), lambdaTree, (MethodSymbol) overriddenElement); - } else { - result = new ReturnNode(tree, node, env.getTypeUtils(), (MethodTree) enclosing); - } + result = new ReturnNode(tree, node, env.getTypeUtils()); returnNodes.add(result); extendWithNode(result); } @@ -2970,7 +3249,7 @@ public Node visitMemberSelect(MemberSelectTree tree, Void p) { extendWithNode(result); return result; } else { - throw new Error("Unexpected element kind: " + element.getKind()); + throw new BugInCF("Unexpected element kind: " + element.getKind()); } } @@ -3022,7 +3301,7 @@ public Node visitThrow(ThrowTree tree, Void p) { @Override public Node visitCompilationUnit(CompilationUnitTree tree, Void p) { - throw new Error("CompilationUnitTree is unexpected in AST to CFG translation"); + throw new BugInCF("CompilationUnitTree is unexpected in AST to CFG translation"); } @Override @@ -3315,7 +3594,7 @@ public Node visitParameterizedType(ParameterizedTypeTree tree, Void p) { @Override public Node visitUnionType(UnionTypeTree tree, Void p) { - throw new Error("UnionTypeTree is unexpected in AST to CFG translation"); + throw new BugInCF("UnionTypeTree is unexpected in AST to CFG translation"); } @Override @@ -3344,7 +3623,7 @@ public Node visitPrimitiveType(PrimitiveTypeTree tree, Void p) { @Override public Node visitTypeParameter(TypeParameterTree tree, Void p) { - throw new Error("TypeParameterTree is unexpected in AST to CFG translation"); + throw new BugInCF("TypeParameterTree is unexpected in AST to CFG translation"); } @Override @@ -3382,7 +3661,7 @@ public Node visitUnary(UnaryTree tree, Void p) { result = new NumericalPlusNode(tree, expr); break; default: - throw new Error("Unexpected kind"); + throw new BugInCF("Unexpected kind: " + kind); } extendWithNode(result); break; @@ -3454,7 +3733,7 @@ public Node visitUnary(UnaryTree tree, Void p) { break; } - throw new Error("Unknown kind (" + kind + ") of unary expression: " + tree); + throw new BugInCF("Unknown kind (" + kind + ") of unary expression: " + tree); } return result; @@ -3517,9 +3796,14 @@ public Node visitVariable(VariableTree tree, Void p) { // see JLS 14.4 - boolean isField = - getCurrentPath().getParentPath() != null - && getCurrentPath().getParentPath().getLeaf().getKind() == Tree.Kind.CLASS; + boolean isField = false; + if (getCurrentPath().getParentPath() != null) { + Tree.Kind kind = TreeUtils.getKindRecordAsClass(getCurrentPath().getParentPath().getLeaf()); + // CLASS includes records. + if (kind == Tree.Kind.CLASS || kind == Tree.Kind.INTERFACE || kind == Tree.Kind.ENUM) { + isField = true; + } + } Node node = null; ClassTree enclosingClass = TreePathUtil.enclosingClass(getCurrentPath()); diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/builder/CFGTranslationPhaseThree.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/builder/CFGTranslationPhaseThree.java index bf83c7f614..cf36d8b029 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/builder/CFGTranslationPhaseThree.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/builder/CFGTranslationPhaseThree.java @@ -13,6 +13,7 @@ import org.checkerframework.dataflow.cfg.block.ExceptionBlockImpl; import org.checkerframework.dataflow.cfg.block.RegularBlockImpl; import org.checkerframework.dataflow.cfg.block.SingleSuccessorBlockImpl; +import org.checkerframework.javacutil.BugInCF; /* --------------------------------------------------------- */ /* Phase Three */ @@ -307,12 +308,12 @@ public BlockImpl getBlock() { } } } - throw new Error("Unreachable"); + throw new BugInCF("Unreachable"); case REGULAR_BLOCK: RegularBlockImpl r = (RegularBlockImpl) pred; return singleSuccessorHolder(r, cur); default: - throw new Error("Unexpected block type " + pred.getType()); + throw new BugInCF("Unexpected block type " + pred.getType()); } } diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/builder/ExtendedNode.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/builder/ExtendedNode.java index 9b00db4177..e1f5bd0093 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/builder/ExtendedNode.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/builder/ExtendedNode.java @@ -69,7 +69,7 @@ public void setTerminatesExecution(boolean terminatesExecution) { * or {@code EXCEPTION_NODE}) */ public Node getNode() { - throw new Error("Do not call"); + throw new BugInCF("Do not call"); } /** @@ -80,7 +80,7 @@ public Node getNode() { * ExtendedNodeType#CONDITIONAL_JUMP} or {@link ExtendedNodeType#UNCONDITIONAL_JUMP}) */ public Label getLabel() { - throw new Error("Do not call"); + throw new BugInCF("Do not call"); } public BlockImpl getBlock() { diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/builder/TryFinallyScopeCell.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/builder/TryFinallyScopeCell.java index 749e6a2c5b..6704662fc5 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/builder/TryFinallyScopeCell.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/builder/TryFinallyScopeCell.java @@ -1,12 +1,17 @@ package org.checkerframework.dataflow.cfg.builder; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.checkerframework.javacutil.BugInCF; /** Storage cell for a single Label, with tracking whether it was accessed. */ class TryFinallyScopeCell { + /** The label. If it is null, then it will be lazily set if {@link #accessLabel} is called. */ private @MonotonicNonNull Label label; + + /** True if the label has been accessed. */ private boolean accessed; + /** Create a TryFinallyScopeCell with no label; the label will be lazily created if needed. */ protected TryFinallyScopeCell() { this.accessed = false; } @@ -27,7 +32,7 @@ public Label accessLabel() { public Label peekLabel() { if (label == null) { - throw new Error("called peekLabel prematurely"); + throw new BugInCF("called peekLabel prematurely"); } return label; } diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/builder/TryFrame.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/builder/TryFrame.java index c80824325b..07ddb3e591 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/builder/TryFrame.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/builder/TryFrame.java @@ -3,10 +3,7 @@ import java.util.Set; import javax.lang.model.type.TypeMirror; -/** - * A TryFrame takes a thrown exception type and maps it to a set of possible control-flow - * successors. - */ +/** A TryFrame maps a thrown exception type to a set of possible control-flow successors. */ interface TryFrame { /** * Given a type of thrown exception, add the set of possible control flow successor {@link Label}s diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/AbstractNodeVisitor.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/AbstractNodeVisitor.java index 8c7dad0160..3700590126 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/AbstractNodeVisitor.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/AbstractNodeVisitor.java @@ -216,6 +216,11 @@ public R visitTernaryExpression(TernaryExpressionNode n, P p) { return visitNode(n, p); } + @Override + public R visitSwitchExpressionNode(SwitchExpressionNode n, P p) { + return visitNode(n, p); + } + @Override public R visitAssignment(AssignmentNode n, P p) { return visitNode(n, p); diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/AssignmentContext.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/AssignmentContext.java deleted file mode 100644 index d62915985b..0000000000 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/AssignmentContext.java +++ /dev/null @@ -1,124 +0,0 @@ -package org.checkerframework.dataflow.cfg.node; - -import com.sun.source.tree.ExpressionTree; -import com.sun.source.tree.MethodTree; -import com.sun.source.tree.Tree; -import com.sun.source.tree.VariableTree; -import javax.lang.model.element.Element; -import javax.lang.model.element.ExecutableElement; -import org.checkerframework.checker.nullness.qual.Nullable; -import org.checkerframework.javacutil.TreeUtils; - -/** - * An assignment context is the left-hand side of an assignment or pseudo-assignment. - * Pseudo-assignments include regular Java assignments, method calls (for all the actual parameters - * which get assigned to their formal parameters), and method return statements. - * - *

The main use of {@link AssignmentContext} is to be able to get the declared type of the - * left-hand side of the assignment for proper type-refinement. - */ -public abstract class AssignmentContext { - - /** An assignment context for an assignment 'lhs = rhs'. */ - public static class AssignmentLhsContext extends AssignmentContext { - - protected final Node node; - - public AssignmentLhsContext(Node node) { - this.node = node; - } - - @Override - public @Nullable Element getElementForType() { - Tree tree = node.getTree(); - if (tree == null) { - return null; - } else if (tree instanceof ExpressionTree) { - return TreeUtils.elementFromUse((ExpressionTree) tree); - } else if (tree instanceof VariableTree) { - return TreeUtils.elementFromDeclaration((VariableTree) tree); - } else { - throw new Error("unexpected tree"); - } - } - - @Override - public @Nullable Tree getContextTree() { - return node.getTree(); - } - } - - /** An assignment context for a method parameter. */ - public static class MethodParameterContext extends AssignmentContext { - - protected final ExecutableElement method; - protected final int paramNum; - - public MethodParameterContext(ExecutableElement method, int paramNum) { - this.method = method; - this.paramNum = paramNum; - } - - @Override - public Element getElementForType() { - return method.getParameters().get(paramNum); - } - - @Override - public @Nullable Tree getContextTree() { - // TODO: what is the right assignment context? We might not have - // a tree for the invoked method. - return null; - } - } - - /** An assignment context for method return statements. */ - public static class MethodReturnContext extends AssignmentContext { - - protected final ExecutableElement method; - protected final Tree ret; - - public MethodReturnContext(MethodTree method) { - this.method = TreeUtils.elementFromDeclaration(method); - this.ret = method.getReturnType(); - } - - @Override - public Element getElementForType() { - return method; - } - - @Override - public Tree getContextTree() { - return ret; - } - } - - /** An assignment context for lambda return statements. */ - public static class LambdaReturnContext extends AssignmentContext { - - protected final ExecutableElement method; - - public LambdaReturnContext(ExecutableElement method) { - this.method = method; - } - - @Override - public Element getElementForType() { - return method; - } - - @Override - public @Nullable Tree getContextTree() { - // TODO: what is the right assignment context? We might not have - // a tree for the invoked method. - return null; - } - } - - /** Returns an {@link Element} that has the type of this assignment context. */ - public abstract @Nullable Element getElementForType(); - - /** Returns the context tree. */ - public abstract @Nullable Tree getContextTree(); -} diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/AssignmentNode.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/AssignmentNode.java index c3c25c262a..d8da27c46d 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/AssignmentNode.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/AssignmentNode.java @@ -9,7 +9,6 @@ import java.util.Collection; import java.util.Objects; import org.checkerframework.checker.nullness.qual.Nullable; -import org.checkerframework.dataflow.cfg.node.AssignmentContext.AssignmentLhsContext; import org.checkerframework.javacutil.TreeUtils; /** @@ -41,7 +40,6 @@ public AssignmentNode(Tree tree, Node target, Node expression) { this.tree = tree; this.lhs = target; this.rhs = expression; - rhs.setAssignmentContext(new AssignmentLhsContext(lhs)); } /** diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/BitwiseAndNode.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/BitwiseAndNode.java index 226df8ed9a..3a0af45cf0 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/BitwiseAndNode.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/BitwiseAndNode.java @@ -14,6 +14,13 @@ */ public class BitwiseAndNode extends BinaryOperationNode { + /** + * Constructs a {@link BitwiseAndNode}. + * + * @param tree the binary tree + * @param left the left operand + * @param right the right operand + */ public BitwiseAndNode(BinaryTree tree, Node left, Node right) { super(tree, left, right); assert tree.getKind() == Tree.Kind.AND; diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/BitwiseComplementNode.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/BitwiseComplementNode.java index a03060d857..56c56f6a57 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/BitwiseComplementNode.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/BitwiseComplementNode.java @@ -14,6 +14,12 @@ */ public class BitwiseComplementNode extends UnaryOperationNode { + /** + * Constructs a {@link BitwiseComplementNode}. + * + * @param tree the tree of the bitwise complement + * @param operand the operand of the bitwise complement + */ public BitwiseComplementNode(UnaryTree tree, Node operand) { super(tree, operand); assert tree.getKind() == Tree.Kind.BITWISE_COMPLEMENT; diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/BitwiseOrNode.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/BitwiseOrNode.java index fcee34e36b..ad70c3ce32 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/BitwiseOrNode.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/BitwiseOrNode.java @@ -14,6 +14,13 @@ */ public class BitwiseOrNode extends BinaryOperationNode { + /** + * Constructs a {@link BitwiseOrNode}. + * + * @param tree the binary tree + * @param left the left operand + * @param right the right operand + */ public BitwiseOrNode(BinaryTree tree, Node left, Node right) { super(tree, left, right); assert tree.getKind() == Tree.Kind.OR; diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/BitwiseXorNode.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/BitwiseXorNode.java index e7fa4ca49a..bd9206f42f 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/BitwiseXorNode.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/BitwiseXorNode.java @@ -14,6 +14,13 @@ */ public class BitwiseXorNode extends BinaryOperationNode { + /** + * Constructs a {@link BitwiseXorNode}. + * + * @param tree the binary tree + * @param left the left operand + * @param right the right operand + */ public BitwiseXorNode(BinaryTree tree, Node left, Node right) { super(tree, left, right); assert tree.getKind() == Tree.Kind.XOR; diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/CaseNode.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/CaseNode.java index eb312b8d86..761f4a32bb 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/CaseNode.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/CaseNode.java @@ -2,12 +2,14 @@ import com.sun.source.tree.CaseTree; import com.sun.source.tree.Tree; -import java.util.Arrays; +import java.util.ArrayList; import java.util.Collection; +import java.util.List; import java.util.Objects; import javax.lang.model.type.TypeKind; import javax.lang.model.util.Types; import org.checkerframework.checker.nullness.qual.Nullable; +import org.plumelib.util.StringsPlume; /** * A node for a case in a switch statement. Although a case has no abstract value, it can imply @@ -21,33 +23,51 @@ public class CaseNode extends Node { /** The tree for this node. */ protected final CaseTree tree; - /** The switch expression. */ - protected final Node switchExpr; - /** The case expression to match the switch expression against. */ - protected final Node caseExpr; + /** + * The Node for the assignment of the switch selector expression to a synthetic local variable. + */ + protected final AssignmentNode selectorExprAssignment; + /** + * The case expressions to match the switch expression against: the operands of (possibly + * multiple) case labels. + */ + protected final List caseExprs; /** * Create a new CaseNode. * * @param tree the tree for this node - * @param switchExpr the switch expression - * @param caseExpr the case expression to match the switch expression against + * @param selectorExprAssignment the switch expression + * @param caseExprs the case expression(s) to match the switch expression against * @param types a factory of utility methods for operating on types */ - public CaseNode(CaseTree tree, Node switchExpr, Node caseExpr, Types types) { + public CaseNode( + CaseTree tree, AssignmentNode selectorExprAssignment, List caseExprs, Types types) { super(types.getNoType(TypeKind.NONE)); assert tree.getKind() == Tree.Kind.CASE; this.tree = tree; - this.switchExpr = switchExpr; - this.caseExpr = caseExpr; + this.selectorExprAssignment = selectorExprAssignment; + this.caseExprs = caseExprs; } - public Node getSwitchOperand() { - return switchExpr; + /** + * The Node for the assignment of the switch selector expression to a synthetic local variable. + * This is used to refine the type of the switch selector expression in a case block. + * + * @return the assignment of the switch selector expression to a synthetic local variable + */ + public AssignmentNode getSwitchOperand() { + return selectorExprAssignment; } - public Node getCaseOperand() { - return caseExpr; + /** + * Gets the nodes corresponding to the case expressions. There can be multiple expressions since + * Java 12. + * + * @return the nodes corresponding to the (potentially multiple) case expressions + */ + public List getCaseOperands() { + return caseExprs; } @Override @@ -62,7 +82,7 @@ public R accept(NodeVisitor visitor, P p) { @Override public String toString() { - return "case " + getCaseOperand() + ":"; + return "case " + StringsPlume.join(", ", getCaseOperands()) + ":"; } @Override @@ -72,16 +92,19 @@ public boolean equals(@Nullable Object obj) { } CaseNode other = (CaseNode) obj; return getSwitchOperand().equals(other.getSwitchOperand()) - && getCaseOperand().equals(other.getCaseOperand()); + && getCaseOperands().equals(other.getCaseOperands()); } @Override public int hashCode() { - return Objects.hash(getSwitchOperand(), getCaseOperand()); + return Objects.hash(getSwitchOperand(), getCaseOperands()); } @Override public Collection getOperands() { - return Arrays.asList(getSwitchOperand(), getCaseOperand()); + ArrayList operands = new ArrayList<>(); + operands.add(getSwitchOperand()); + operands.addAll(getCaseOperands()); + return operands; } } diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/FloatingDivisionNode.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/FloatingDivisionNode.java index 817eddf551..12e91bbdcf 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/FloatingDivisionNode.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/FloatingDivisionNode.java @@ -14,6 +14,13 @@ */ public class FloatingDivisionNode extends BinaryOperationNode { + /** + * Constructs a {@link FloatingDivisionNode}. + * + * @param tree the binary tree + * @param left the left operand + * @param right the right operand + */ public FloatingDivisionNode(BinaryTree tree, Node left, Node right) { super(tree, left, right); assert tree.getKind() == Tree.Kind.DIVIDE; diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/FloatingRemainderNode.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/FloatingRemainderNode.java index e59eb56a2d..134d18caa5 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/FloatingRemainderNode.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/FloatingRemainderNode.java @@ -14,6 +14,13 @@ */ public class FloatingRemainderNode extends BinaryOperationNode { + /** + * Constructs a {@link FloatingRemainderNode}. + * + * @param tree the binary tree + * @param left the left operand + * @param right the right operand + */ public FloatingRemainderNode(BinaryTree tree, Node left, Node right) { super(tree, left, right); assert tree.getKind() == Tree.Kind.REMAINDER; diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/GreaterThanNode.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/GreaterThanNode.java index 80d5ccefea..771d2992d2 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/GreaterThanNode.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/GreaterThanNode.java @@ -14,6 +14,13 @@ */ public class GreaterThanNode extends BinaryOperationNode { + /** + * Constructs a {@link GreaterThanNode}. + * + * @param tree the binary tree + * @param left the left operand + * @param right the right operand + */ public GreaterThanNode(BinaryTree tree, Node left, Node right) { super(tree, left, right); assert tree.getKind() == Tree.Kind.GREATER_THAN; diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/GreaterThanOrEqualNode.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/GreaterThanOrEqualNode.java index d45fa3c329..f6ecab0ab9 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/GreaterThanOrEqualNode.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/GreaterThanOrEqualNode.java @@ -14,6 +14,13 @@ */ public class GreaterThanOrEqualNode extends BinaryOperationNode { + /** + * Constructs a {@link GreaterThanOrEqualNode}. + * + * @param tree the binary tree + * @param left the left operand + * @param right the right operand + */ public GreaterThanOrEqualNode(BinaryTree tree, Node left, Node right) { super(tree, left, right); assert tree.getKind() == Tree.Kind.GREATER_THAN_EQUAL; diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/IntegerDivisionNode.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/IntegerDivisionNode.java index 4c84e95636..ff6e5bd23e 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/IntegerDivisionNode.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/IntegerDivisionNode.java @@ -14,6 +14,13 @@ */ public class IntegerDivisionNode extends BinaryOperationNode { + /** + * Constructs an {@link IntegerDivisionNode}. + * + * @param tree the binary tree + * @param left the left operand + * @param right the right operand + */ public IntegerDivisionNode(BinaryTree tree, Node left, Node right) { super(tree, left, right); assert tree.getKind() == Tree.Kind.DIVIDE; diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/IntegerRemainderNode.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/IntegerRemainderNode.java index 9507e85aa5..6a0a1b3f0f 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/IntegerRemainderNode.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/IntegerRemainderNode.java @@ -14,6 +14,13 @@ */ public class IntegerRemainderNode extends BinaryOperationNode { + /** + * Constructs an {@link IntegerRemainderNode}. + * + * @param tree the binary tree + * @param left the left operand + * @param right the right operand + */ public IntegerRemainderNode(BinaryTree tree, Node left, Node right) { super(tree, left, right); assert tree.getKind() == Tree.Kind.REMAINDER; diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/LeftShiftNode.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/LeftShiftNode.java index 54ca5d2ace..b6b38e8506 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/LeftShiftNode.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/LeftShiftNode.java @@ -14,6 +14,13 @@ */ public class LeftShiftNode extends BinaryOperationNode { + /** + * Constructs a {@link LeftShiftNode}. + * + * @param tree the binary tree + * @param left the left operand + * @param right the right operand + */ public LeftShiftNode(BinaryTree tree, Node left, Node right) { super(tree, left, right); assert tree.getKind() == Tree.Kind.LEFT_SHIFT; diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/LessThanNode.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/LessThanNode.java index a5e7f3c5cc..8b5595938f 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/LessThanNode.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/LessThanNode.java @@ -16,6 +16,13 @@ */ public class LessThanNode extends BinaryOperationNode { + /** + * Constructs a {@link LessThanNode}. + * + * @param tree the binary tree + * @param left the left operand + * @param right the right operand + */ public LessThanNode(BinaryTree tree, Node left, Node right) { super(tree, left, right); assert tree.getKind() == Tree.Kind.LESS_THAN; diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/LessThanOrEqualNode.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/LessThanOrEqualNode.java index 50a211272d..5f13d00445 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/LessThanOrEqualNode.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/LessThanOrEqualNode.java @@ -14,6 +14,13 @@ */ public class LessThanOrEqualNode extends BinaryOperationNode { + /** + * Constructs a {@link LessThanOrEqualNode}. + * + * @param tree the binary tree + * @param left the left operand + * @param right the right operand + */ public LessThanOrEqualNode(BinaryTree tree, Node left, Node right) { super(tree, left, right); assert tree.getKind() == Tree.Kind.LESS_THAN_EQUAL; diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/MethodInvocationNode.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/MethodInvocationNode.java index bbce8e15b5..c624a8e2e2 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/MethodInvocationNode.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/MethodInvocationNode.java @@ -10,7 +10,6 @@ import java.util.List; import java.util.Objects; import org.checkerframework.checker.nullness.qual.Nullable; -import org.checkerframework.dataflow.cfg.node.AssignmentContext.MethodParameterContext; import org.checkerframework.javacutil.TreeUtils; import org.plumelib.util.StringsPlume; @@ -66,13 +65,6 @@ public MethodInvocationNode( this.target = target; this.arguments = arguments; this.treePath = treePath; - - // set assignment contexts for parameters - int i = 0; - for (Node arg : arguments) { - AssignmentContext ctx = new MethodParameterContext(target.getMethod(), i++); - arg.setAssignmentContext(ctx); - } } public MethodInvocationNode(MethodAccessNode target, List arguments, TreePath treePath) { diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/Node.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/Node.java index 4f7aeadc38..26dc123dac 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/Node.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/Node.java @@ -45,9 +45,6 @@ public abstract class Node implements UniqueId { /** Is this node an l-value? */ protected boolean lvalue = false; - /** The assignment context of this node. See {@link AssignmentContext}. */ - protected @Nullable AssignmentContext assignmentContext; - /** * Does this node represent a tree that appears in the source code (true) or one that the CFG * builder added while desugaring (false). @@ -148,15 +145,6 @@ public void setInSource(boolean inSrc) { inSource = inSrc; } - /** The assignment context for the node. */ - public @Nullable AssignmentContext getAssignmentContext() { - return assignmentContext; - } - - public void setAssignmentContext(AssignmentContext assignmentContext) { - this.assignmentContext = assignmentContext; - } - /** * Returns a collection containing all of the operand {@link Node}s of this {@link Node}. * diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/NodeVisitor.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/NodeVisitor.java index 73c07f5335..8991d67bf2 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/NodeVisitor.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/NodeVisitor.java @@ -91,6 +91,8 @@ public interface NodeVisitor { R visitTernaryExpression(TernaryExpressionNode n, P p); + R visitSwitchExpressionNode(SwitchExpressionNode n, P p); + R visitAssignment(AssignmentNode n, P p); R visitLocalVariable(LocalVariableNode n, P p); diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/NotEqualNode.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/NotEqualNode.java index b05c06259d..ee0548c27a 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/NotEqualNode.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/NotEqualNode.java @@ -14,6 +14,13 @@ */ public class NotEqualNode extends BinaryOperationNode { + /** + * Constructs a {@link NotEqualNode}. + * + * @param tree the binary tree + * @param left the left operand + * @param right the right operand + */ public NotEqualNode(BinaryTree tree, Node left, Node right) { super(tree, left, right); assert tree.getKind() == Tree.Kind.NOT_EQUAL_TO; diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/NullChkNode.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/NullChkNode.java index 840614bf47..6861aa339f 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/NullChkNode.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/NullChkNode.java @@ -15,10 +15,17 @@ * */ public class NullChkNode extends Node { - + /** The entire tree of the null check */ protected final Tree tree; + /** The operand of the null check */ protected final Node operand; + /** + * Constructs a {@link NullChkNode}. + * + * @param tree the nullchk tree + * @param operand the operand of the null check + */ public NullChkNode(Tree tree, Node operand) { super(TreeUtils.typeOf(tree)); assert tree.getKind() == Tree.Kind.OTHER; diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/NumericalAdditionNode.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/NumericalAdditionNode.java index a3f9c0c921..ced8caf3be 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/NumericalAdditionNode.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/NumericalAdditionNode.java @@ -14,6 +14,13 @@ */ public class NumericalAdditionNode extends BinaryOperationNode { + /** + * Constructs a {@link NumericalAdditionNode}. + * + * @param tree the binary tree + * @param left the left operand + * @param right the right operand + */ public NumericalAdditionNode(BinaryTree tree, Node left, Node right) { super(tree, left, right); assert tree.getKind() == Tree.Kind.PLUS || tree.getKind() == Tree.Kind.PLUS_ASSIGNMENT; diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/NumericalMinusNode.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/NumericalMinusNode.java index c5eb5c067c..c5b2e5619f 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/NumericalMinusNode.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/NumericalMinusNode.java @@ -14,6 +14,12 @@ */ public class NumericalMinusNode extends UnaryOperationNode { + /** + * Constructs a {@link NumericalMinusNode}. + * + * @param tree the unary tree + * @param operand the operand of the unary minus + */ public NumericalMinusNode(UnaryTree tree, Node operand) { super(tree, operand); assert tree.getKind() == Tree.Kind.UNARY_MINUS; diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/NumericalMultiplicationNode.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/NumericalMultiplicationNode.java index 7b2d90ace1..3dabcaf1dd 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/NumericalMultiplicationNode.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/NumericalMultiplicationNode.java @@ -14,6 +14,13 @@ */ public class NumericalMultiplicationNode extends BinaryOperationNode { + /** + * Constructs a {@link NumericalMultiplicationNode}. + * + * @param tree the binary tree + * @param left the left operand + * @param right the right operand + */ public NumericalMultiplicationNode(BinaryTree tree, Node left, Node right) { super(tree, left, right); assert tree.getKind() == Tree.Kind.MULTIPLY; diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/NumericalPlusNode.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/NumericalPlusNode.java index c8a11f27c6..a5770ebf06 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/NumericalPlusNode.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/NumericalPlusNode.java @@ -14,6 +14,12 @@ */ public class NumericalPlusNode extends UnaryOperationNode { + /** + * Constructs a {@link NumericalPlusNode}. + * + * @param tree The tree of the whole operation + * @param operand The operand of the operation + */ public NumericalPlusNode(UnaryTree tree, Node operand) { super(tree, operand); assert tree.getKind() == Tree.Kind.UNARY_PLUS; diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/NumericalSubtractionNode.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/NumericalSubtractionNode.java index 6dc1af4fae..5d2576bb7b 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/NumericalSubtractionNode.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/NumericalSubtractionNode.java @@ -14,6 +14,13 @@ */ public class NumericalSubtractionNode extends BinaryOperationNode { + /** + * Constructs a {@link NumericalSubtractionNode}. + * + * @param tree the binary tree + * @param left the left operand + * @param right the right operand + */ public NumericalSubtractionNode(BinaryTree tree, Node left, Node right) { super(tree, left, right); assert tree.getKind() == Tree.Kind.MINUS; diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/ObjectCreationNode.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/ObjectCreationNode.java index 791e1ee164..1ac1e97068 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/ObjectCreationNode.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/ObjectCreationNode.java @@ -5,7 +5,6 @@ import java.util.Collection; import java.util.List; import java.util.Objects; -import javax.lang.model.element.ExecutableElement; import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.javacutil.TreeUtils; import org.plumelib.util.StringsPlume; @@ -36,16 +35,6 @@ public ObjectCreationNode( this.constructor = constructor; this.arguments = arguments; this.classbody = classbody; - - // set assignment contexts for parameters - int i = 0; - ExecutableElement elem = TreeUtils.elementFromUse(tree); - if (elem != null) { - for (Node arg : arguments) { - AssignmentContext ctx = new AssignmentContext.MethodParameterContext(elem, i++); - arg.setAssignmentContext(ctx); - } - } } public Node getConstructor() { diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/ReturnNode.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/ReturnNode.java index 8c189a3f9f..ea0c308c16 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/ReturnNode.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/ReturnNode.java @@ -10,8 +10,6 @@ import javax.lang.model.type.TypeKind; import javax.lang.model.util.Types; import org.checkerframework.checker.nullness.qual.Nullable; -import org.checkerframework.dataflow.cfg.node.AssignmentContext.LambdaReturnContext; -import org.checkerframework.dataflow.cfg.node.AssignmentContext.MethodReturnContext; /** * A node for a return statement: @@ -25,30 +23,60 @@ */ public class ReturnNode extends Node { - protected final ReturnTree tree; + /** The return tree. */ + protected final ReturnTree returnTree; + + /** The node of the returned expression. */ protected final @Nullable Node result; - public ReturnNode(ReturnTree t, @Nullable Node result, Types types, MethodTree methodTree) { + /** + * Creates a node for the given return statement. + * + * @param returnTree return tree + * @param result the returned expression + * @param types types util + */ + public ReturnNode(ReturnTree returnTree, @Nullable Node result, Types types) { super(types.getNoType(TypeKind.NONE)); this.result = result; - tree = t; - if (result != null) { - result.setAssignmentContext(new MethodReturnContext(methodTree)); - } + this.returnTree = returnTree; + } + + /** + * Creates a node for the given return statement. + * + * @param returnTree return tree + * @param result the returned expression + * @param types types util + * @param methodTree method tree + * @deprecated Use {@code #ReturnNode(ReturnTree, Node, Types, LambdaExpressionTree, + * MethodSymbol)} instead. + */ + @Deprecated + public ReturnNode( + ReturnTree returnTree, @Nullable Node result, Types types, MethodTree methodTree) { + this(returnTree, result, types); } + /** + * Creates a node for the given return statement. + * + * @param returnTree return tree + * @param result the returned expression + * @param types types util + * @param lambda lambda + * @param methodSymbol methodSymbol + * @deprecated Use {@code #ReturnNode(ReturnTree, Node, Types, LambdaExpressionTree, + * MethodSymbol)} instead. + */ + @Deprecated public ReturnNode( - ReturnTree t, + ReturnTree returnTree, @Nullable Node result, Types types, LambdaExpressionTree lambda, MethodSymbol methodSymbol) { - super(types.getNoType(TypeKind.NONE)); - this.result = result; - tree = t; - if (result != null) { - result.setAssignmentContext(new LambdaReturnContext(methodSymbol)); - } + this(returnTree, result, types); } /** The result of the return node, {@code null} otherwise. */ @@ -58,7 +86,7 @@ public ReturnNode( @Override public ReturnTree getTree() { - return tree; + return returnTree; } @Override diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/SignedRightShiftNode.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/SignedRightShiftNode.java index c9fbf81dac..849b7fafbf 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/SignedRightShiftNode.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/SignedRightShiftNode.java @@ -14,6 +14,13 @@ */ public class SignedRightShiftNode extends BinaryOperationNode { + /** + * Constructs a {@link SignedRightShiftNode}. + * + * @param tree the binary tree + * @param left the left operand + * @param right the right operand + */ public SignedRightShiftNode(BinaryTree tree, Node left, Node right) { super(tree, left, right); assert tree.getKind() == Tree.Kind.RIGHT_SHIFT; diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/StringConcatenateAssignmentNode.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/StringConcatenateAssignmentNode.java index c555820631..e0ecb410a8 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/StringConcatenateAssignmentNode.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/StringConcatenateAssignmentNode.java @@ -15,10 +15,20 @@ * */ public class StringConcatenateAssignmentNode extends Node { + /** The entire tree of the assignment */ protected final Tree tree; + /** The left-hand side of the assignment */ protected final Node left; + /** The right-hand side of the assignment */ protected final Node right; + /** + * Constructs an {@link StringConcatenateAssignmentNode}. + * + * @param tree the binary tree of the assignment + * @param left the left-hand side + * @param right the right-hand side + */ public StringConcatenateAssignmentNode(Tree tree, Node left, Node right) { super(TreeUtils.typeOf(tree)); assert tree.getKind() == Tree.Kind.PLUS_ASSIGNMENT; diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/StringConcatenateNode.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/StringConcatenateNode.java index 95e8d6d17d..c7d0b2d0fb 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/StringConcatenateNode.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/StringConcatenateNode.java @@ -14,6 +14,13 @@ */ public class StringConcatenateNode extends BinaryOperationNode { + /** + * Constructs a {@link StringConcatenateNode}. + * + * @param tree the binary tree + * @param left the left operand + * @param right the right operand + */ public StringConcatenateNode(BinaryTree tree, Node left, Node right) { super(tree, left, right); assert tree.getKind() == Tree.Kind.PLUS; diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/SwitchExpressionNode.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/SwitchExpressionNode.java new file mode 100644 index 0000000000..67ae65389d --- /dev/null +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/SwitchExpressionNode.java @@ -0,0 +1,95 @@ +package org.checkerframework.dataflow.cfg.node; + +import com.sun.source.tree.Tree; +import java.util.Collection; +import java.util.Collections; +import java.util.Objects; +import javax.lang.model.type.TypeMirror; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.javacutil.BugInCF; + +/** A node for a switch expression. */ +public class SwitchExpressionNode extends Node { + + /** The {@code SwitchExpressionTree} corresponding to this node. */ + private final Tree switchExpressionTree; + + /** + * This is a variable created by dataflow to which each result expression of the switch expression + * is assigned. Its value should be used for the value of the switch expression. + */ + private final LocalVariableNode switchExpressionVar; + + /** + * Creates a new SwitchExpressionNoode. + * + * @param type the type of the node + * @param switchExpressionTree the {@code SwitchExpressionTree} for this node + * @param switchExpressionVar a variable created by dataflow to which each result expression of + * the switch expression is assigned. Its value should be used for the value of the switch + * expression + */ + public SwitchExpressionNode( + TypeMirror type, Tree switchExpressionTree, LocalVariableNode switchExpressionVar) { + super(type); + + if (!switchExpressionTree.getKind().name().equals("SWITCH_EXPRESSION")) { + throw new BugInCF( + "switchExpressionTree is not a SwitchExpressionTree found tree with kind %s instead.", + switchExpressionTree.getKind()); + } + + this.switchExpressionTree = switchExpressionTree; + this.switchExpressionVar = switchExpressionVar; + } + + @Override + public Tree getTree() { + return switchExpressionTree; + } + + /** + * This is a variable created by dataflow to which each result expression of the switch expression + * is assigned. Its value should be used for the value of the switch expression. + * + * @return the variable for this switch expression + */ + public LocalVariableNode getSwitchExpressionVar() { + return switchExpressionVar; + } + + @Override + public R accept(NodeVisitor visitor, P p) { + return visitor.visitSwitchExpressionNode(this, p); + } + + @Override + public Collection getOperands() { + return Collections.singleton(switchExpressionVar); + } + + @Override + public String toString() { + return "SwitchExpressionNode{" + + "switchExpressionTree=" + + switchExpressionTree + + ", switchExpressionVar=" + + switchExpressionVar + + '}'; + } + + @Override + public boolean equals(@Nullable Object obj) { + if (!(obj instanceof SwitchExpressionNode)) { + return false; + } + SwitchExpressionNode other = (SwitchExpressionNode) obj; + return getTree().equals(other.getTree()) + && getSwitchExpressionVar().equals(other.getSwitchExpressionVar()); + } + + @Override + public int hashCode() { + return Objects.hash(getTree(), getSwitchExpressionVar()); + } +} diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/TernaryExpressionNode.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/TernaryExpressionNode.java index 6e6f3fa80b..919a7f2eb5 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/TernaryExpressionNode.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/TernaryExpressionNode.java @@ -17,33 +17,87 @@ */ public class TernaryExpressionNode extends Node { + /** The {@code ConditionalExpressionTree} corresponding to this node */ protected final ConditionalExpressionTree tree; + + /** Node representing the condition checked by the expression */ protected final Node condition; + + /** Node representing the "then" case of the expression */ protected final Node thenOperand; + + /** Node representing the "else" case of the expression */ protected final Node elseOperand; + /** + * This is a variable created by dataflow to which each case expression of the ternary expression + * is assigned. Its value should be used for the value of the switch expression. + */ + private final LocalVariableNode ternaryExpressionVar; + + /** + * Creates a new TernaryExpressionNode. + * + * @param tree the {@code ConditionalExpressionTree} for the node + * @param condition node representing the condition checked by the expression + * @param thenOperand node representing the "then" case of the expression + * @param elseOperand node representing the "else" case of the expression + * @param ternaryExpressionVar a variable created by dataflow to which each case expression of the + * ternary expression is assigned. Its value should be used for the value of the switch + * expression. + */ public TernaryExpressionNode( - ConditionalExpressionTree tree, Node condition, Node thenOperand, Node elseOperand) { + ConditionalExpressionTree tree, + Node condition, + Node thenOperand, + Node elseOperand, + LocalVariableNode ternaryExpressionVar) { super(TreeUtils.typeOf(tree)); assert tree.getKind() == Tree.Kind.CONDITIONAL_EXPRESSION; this.tree = tree; this.condition = condition; this.thenOperand = thenOperand; this.elseOperand = elseOperand; + this.ternaryExpressionVar = ternaryExpressionVar; } + /** + * Gets the node representing the conditional operand for this node + * + * @return the condition operand node + */ public Node getConditionOperand() { return condition; } + /** + * Gets the node representing the "then" operand for this node + * + * @return the "then" operand node + */ public Node getThenOperand() { return thenOperand; } + /** + * Gets the node representing the "else" operand for this node + * + * @return the "else" operand node + */ public Node getElseOperand() { return elseOperand; } + /** + * This is a variable created by dataflow to which each case expression of the ternary expression + * is assigned. Its value should be used for the value of the switch expression. + * + * @return the variable for this ternary expression + */ + public LocalVariableNode getTernaryExpressionVar() { + return ternaryExpressionVar; + } + @Override public ConditionalExpressionTree getTree() { return tree; diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/UnsignedRightShiftNode.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/UnsignedRightShiftNode.java index db8ed7de26..a87002eb16 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/UnsignedRightShiftNode.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/UnsignedRightShiftNode.java @@ -14,6 +14,13 @@ */ public class UnsignedRightShiftNode extends BinaryOperationNode { + /** + * Constructs an {@link UnsignedRightShiftNode} + * + * @param tree the binary tree + * @param left the left operand + * @param right the right operand + */ public UnsignedRightShiftNode(BinaryTree tree, Node left, Node right) { super(tree, left, right); assert tree.getKind() == Tree.Kind.UNSIGNED_RIGHT_SHIFT; diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/constantpropagation/Constant.java b/dataflow/src/main/java/org/checkerframework/dataflow/constantpropagation/Constant.java index 361cc1a2f1..33a032e970 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/constantpropagation/Constant.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/constantpropagation/Constant.java @@ -4,6 +4,7 @@ import org.checkerframework.checker.nullness.qual.EnsuresNonNullIf; import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.dataflow.analysis.AbstractValue; +import org.checkerframework.javacutil.BugInCF; public class Constant implements AbstractValue { @@ -118,7 +119,7 @@ public String toString() { assert isConstant() : "@AssumeAssertion(nullness)"; return value.toString(); default: - throw new Error("Unexpected type"); + throw new BugInCF("Unexpected type: " + type); } } } diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/expression/BinaryOperation.java b/dataflow/src/main/java/org/checkerframework/dataflow/expression/BinaryOperation.java index f4607dad53..2106e0c600 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/expression/BinaryOperation.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/expression/BinaryOperation.java @@ -7,6 +7,7 @@ import org.checkerframework.dataflow.analysis.Store; import org.checkerframework.dataflow.cfg.node.BinaryOperationNode; import org.checkerframework.javacutil.AnnotationProvider; +import org.checkerframework.javacutil.BugInCF; /** JavaExpression for binary operations. */ public class BinaryOperation extends JavaExpression { @@ -214,7 +215,7 @@ private String operationKindToString(Tree.Kind operationKind) { case XOR: return "^"; default: - throw new Error("unhandled " + operationKind); + throw new BugInCF("unhandled " + operationKind); } } diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/expression/FieldAccess.java b/dataflow/src/main/java/org/checkerframework/dataflow/expression/FieldAccess.java index e0316c33d5..d04f84c84a 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/expression/FieldAccess.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/expression/FieldAccess.java @@ -8,6 +8,7 @@ import org.checkerframework.dataflow.analysis.Store; import org.checkerframework.dataflow.cfg.node.FieldAccessNode; import org.checkerframework.javacutil.AnnotationProvider; +import org.checkerframework.javacutil.BugInCF; import org.checkerframework.javacutil.ElementUtils; import org.checkerframework.javacutil.TypesUtils; @@ -72,8 +73,8 @@ public FieldAccess(JavaExpression receiver, TypeMirror type, VariableElement fie this.field = fieldElement; String fieldName = fieldElement.toString(); if (fieldName.equals("class") || fieldName.equals("this")) { - Error e = - new Error( + BugInCF e = + new BugInCF( String.format( "bad field name \"%s\" in new FieldAccess(%s, %s, %s)%n", fieldName, receiver, type, fieldElement)); diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/expression/JavaExpression.java b/dataflow/src/main/java/org/checkerframework/dataflow/expression/JavaExpression.java index 19699e14f1..f9d4521841 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/expression/JavaExpression.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/expression/JavaExpression.java @@ -109,7 +109,6 @@ public boolean containsUnknown() { * @param provider an annotation provider (a type factory) * @return true if all the expressions in the list are deterministic */ - @SuppressWarnings("nullness:dereference.of.nullable") // flow within a lambda public static boolean listIsDeterministic( List list, AnnotationProvider provider) { return list.stream().allMatch(je -> je == null || je.isDeterministic(provider)); @@ -195,7 +194,6 @@ static boolean syntacticEqualsList( * @return true if and only if the list contains a JavaExpression that is syntactically equal to * {@code other} */ - @SuppressWarnings("nullness:dereference.of.nullable") // flow within a lambda public static boolean listContainsSyntacticEqualJavaExpression( List list, JavaExpression other) { return list.stream() diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/expression/UnaryOperation.java b/dataflow/src/main/java/org/checkerframework/dataflow/expression/UnaryOperation.java index 6499ed63b3..fe6c9e1b9b 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/expression/UnaryOperation.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/expression/UnaryOperation.java @@ -7,6 +7,7 @@ import org.checkerframework.dataflow.analysis.Store; import org.checkerframework.dataflow.cfg.node.UnaryOperationNode; import org.checkerframework.javacutil.AnnotationProvider; +import org.checkerframework.javacutil.BugInCF; /** JavaExpression for unary operations. */ public class UnaryOperation extends JavaExpression { @@ -134,7 +135,7 @@ public String toString() { case UNARY_PLUS: return "+" + operandString; default: - throw new Error("Unrecognized unary operation kind " + operationKind); + throw new BugInCF("Unrecognized unary operation kind " + operationKind); } } diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/expression/ValueLiteral.java b/dataflow/src/main/java/org/checkerframework/dataflow/expression/ValueLiteral.java index 11a83ba801..7b3a0ae9be 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/expression/ValueLiteral.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/expression/ValueLiteral.java @@ -8,6 +8,7 @@ import org.checkerframework.dataflow.analysis.Store; import org.checkerframework.dataflow.cfg.node.ValueLiteralNode; import org.checkerframework.javacutil.AnnotationProvider; +import org.checkerframework.javacutil.BugInCF; import org.checkerframework.javacutil.TypesUtils; /** JavaExpression for literals. */ @@ -50,11 +51,11 @@ public ValueLiteral(TypeMirror type, Object value) { public ValueLiteral negate() { if (TypesUtils.isIntegralPrimitive(type)) { if (value == null) { - throw new Error("null value of integral type " + type); + throw new BugInCF("null value of integral type " + type); } return new ValueLiteral(type, negateBoxedPrimitive(value)); } - throw new Error(String.format("cannot negate: %s type=%s", this, type)); + throw new BugInCF(String.format("cannot negate: %s type=%s", this, type)); } /** @@ -86,7 +87,7 @@ private Object negateBoxedPrimitive(Object o) { assert value.equals(NEGATIVE_LONG_MIN_VALUE); return Long.MIN_VALUE; } - throw new Error("Cannot be negated: " + o + " " + o.getClass()); + throw new BugInCF("Cannot be negated: " + o + " " + o.getClass()); } /** diff --git a/dataflow/tests/live-variable/Expected.txt b/dataflow/tests/live-variable/Expected.txt index b5700e43b7..adf354feaf 100644 --- a/dataflow/tests/live-variable/Expected.txt +++ b/dataflow/tests/live-variable/Expected.txt @@ -4,7 +4,12 @@ 4 -> 10 ELSE_TO_BOTH 8 -> 11 EACH_TO_EACH 10 -> 11 EACH_TO_EACH -11 -> 0 EACH_TO_EACH +11 -> 12 EACH_TO_EACH +12 -> 13 EACH_TO_EACH +12 -> 15 ArithmeticException +13 -> 17 EACH_TO_EACH +15 -> 17 EACH_TO_EACH +17 -> 0 EACH_TO_EACH 2: Process order: 1 @@ -31,7 +36,7 @@ a [ LocalVariable ] 0 [ IntegerLiteral ] (a > 0) [ GreaterThan ] ~~~~~~~~~ -TransferInput#22 +TransferInput#49 After: live variables = a, b, c 4: @@ -52,7 +57,7 @@ c [ LocalVariable ] (a + c) [ NumericalAddition ] d = (a + c) [ Assignment ] ~~~~~~~~~ -TransferInput#6 +TransferInput#33 After: live variables = a 10: @@ -66,7 +71,7 @@ b [ LocalVariable ] (a + b) [ NumericalAddition ] e = (a + b) [ Assignment ] ~~~~~~~~~ -TransferInput#5 +TransferInput#32 After: live variables = a 11: @@ -74,6 +79,58 @@ Process order: 6 AnalysisResult#11 Before: live variables = a ~~~~~~~~~ +f [ VariableDeclaration ] +b [ LocalVariable ] +0 [ IntegerLiteral ] +b = 0 [ Assignment ] +marker (start of try statement #0) [ Marker ] +marker (start of try block #0) [ Marker ] +f [ LocalVariable ] +1 [ IntegerLiteral ] +a [ LocalVariable ] +~~~~~~~~~ +TransferInput#21 +After: live variables = a, b + +12: +Process order: 7 +AnalysisResult#13 +Before: live variables = a +~~~~~~~~~ +(1 / a) [ IntegerDivision ] + +13: +Process order: 8 +AnalysisResult#15 +Before: live variables = a +~~~~~~~~~ +(1 / a) [ IntegerDivision ] +f = (1 / a) [ Assignment ] +marker (end of try block #0) [ Marker ] +~~~~~~~~~ +TransferInput#5 +After: live variables = a + +15: +Process order: 9 +AnalysisResult#17 +Before: live variables = a, b +~~~~~~~~~ +marker (start of catch block for ArithmeticException #0) [ Marker ] +e [ VariableDeclaration ] +f [ LocalVariable ] +b [ LocalVariable ] +f = b [ Assignment ] +marker (end of catch block for ArithmeticException #0) [ Marker ] +~~~~~~~~~ +TransferInput#6 +After: live variables = a + +17: +Process order: 10 +AnalysisResult#19 +Before: live variables = a +~~~~~~~~~ a [ LocalVariable ] return a [ Return ] ~~~~~~~~~ @@ -81,8 +138,8 @@ TransferInput#1 After: live variables = none 0: -Process order: 7 -AnalysisResult#13 +Process order: 11 +AnalysisResult#21 Before: live variables = none ~~~~~~~~~ diff --git a/dataflow/tests/live-variable/Test.java b/dataflow/tests/live-variable/Test.java index 2009b54652..0248330e8c 100644 --- a/dataflow/tests/live-variable/Test.java +++ b/dataflow/tests/live-variable/Test.java @@ -7,6 +7,13 @@ public int test() { } else { int e = a + b; } + int f; + b = 0; + try { + f = 1 / a; + } catch (ArithmeticException e) { + f = b; + } return a; } } diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index b22943f9b1..f31be07c50 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -1,8 +1,147 @@ -Version 3.16.0 (July 9, 2021) ------------------------------ +Version 3.?.? (?, 2021) +------------------------------- + +**User-visible changes:** + +The Checker Framework Gradle Plugin now works incrementally: if you change just +one source file, then Gradle will recompile just that file rather than all +files. + +**Implementation details:** + +**Closed issues:** + + +Version 3.21.0 (December 17, 2021) +------------------------------- + +**User-visible changes:** + +The Checker Framework now more precisely computes the type of a switch expression. + +**Implementation details:** + +The dataflow framework now analyzes switch expressions and switch statements +that use the new `->` case syntax. To do so, a new node, SwitchExpressionNode, +was added. + +**Closed issues:** +#2373, #4934, #4977, #4979, #4987. + +Version 3.20.0 (December 6, 2021) +------------------------------- + +**User-visible changes:** + +The Checker Framework now runs on code that contains switch expressions and +switch statements that use the new `->` case syntax, but treats them +conservatively. A future version will improve precision. + +**Implementation details:** + +The dataflow framework can be run on code that contains switch expressions and +switch statements that use the new `->` case syntax, but it does not yet +analyze the cases in a switch expression and it treats `->` as `:`. A future +version will do so. + +Removed methods and classes that have been deprecated for more than one year: + * Old way of constructing qualifier hierarchies + * `@SuppressWarningsKeys` + * `RegularBlock.getContents()` + * `TestUtilities.testBooleanProperty()` + * `CFAbstractTransfer.getValueWithSameAnnotations()` + +**Closed issues:** +#4911, #4948, #4965. + + +Version 3.19.0 (November 1, 2021) +------------------------------- + +**User-visible changes:** + +The Checker Framework runs under JDK 17 -- that is, it runs on a version 17 JVM. +The Checker Framework also continues to run under JDK 8 and JDK 11. New +command-line argument `-ApermitUnsupportedJdkVersion` lets you run the Checker +Framework on any JDK (version 8 or greater) without a warning about an +unsupported JDK version. The Checker Framework does not yet run on code that +contains switch expressions. + +**Implementation details:** + +Removed `org.checkerframework.framework.type.VisitorState` +Removed `AnnotatedTypeFactory#postTypeVarSubstitution` + +Deprecated methods in AnnotatedTypeFactory: +* `getCurrentClassTree` +* `getCurrentMethodReceiver` + +**Closed issues:** +#4932, #4924, #4908, #3014. + + +Version 3.18.1 (October 4, 2021) +------------------------------- + +**Closed issues:** +#4902 and #4903. + + +Version 3.18.0 (September 1, 2021) +------------------------------- + +**User-visible changes:** + +Java records are type-checked. Thanks to Neil Brown. + +**Closed issues:** +#4838, #4843, #4852, #4853, #4861, #4876, #4877, #4878, #4878, #4889, #4889. + + +Version 3.17.0 (August 3, 2021) +------------------------------- **User-visible changes:** +`-Ainfer` can now infer postcondition annotations that reference formal parameters +(e.g. `"#1"`, `"#2"`) and the receiver (`"this"`). + +**Implementation details:** + +Method renamings and signature changes (old methods are removed) in `GenericAnnotatedTypeFactory`: +* `getPreconditionAnnotation(VariableElement, AnnotatedTypeMirror)` => `getPreconditionAnnotations(String, AnnotatedTypeMirror, AnnotatedTypeMirror)` +* `getPostconditionAnnotation(VariableElement, AnnotatedTypeMirror, List)` => `getPostconditionAnnotations(String, AnnotatedTypeMirror, AnnotatedTypeMirror, List)` +* `getPreOrPostconditionAnnotation(VariableElement, AnnotatedTypeMirror, Analysis.BeforeOrAfter, List)` => `getPreOrPostconditionAnnotations(String, AnnotatedTypeMirror, AnnotatedTypeMirror, Analysis.BeforeOrAfter, List)` +* `requiresOrEnsuresQualifierAnno(VariableElement, AnnotationMirror, Analysis.BeforeOrAfter)` => `createRequiresOrEnsuresQualifier(String, AnnotationMirror, AnnotatedTypeMirror, Analysis.BeforeOrAfter, List)` + +Method renamings and signature changes (old method is removed) in `WholeProgramInferenceStorage`: +* `getPreOrPostconditionsForField(Analysis.BeforeOrAfter, ExecutableElement, VariableElement, AnnotatedTypeFactory)` => `getPreOrPostconditions(Analysis.BeforeOrAfter, ExecutableElement, String, AnnotatedTypeMirror, AnnotatedTypeFactory)` + +Method renamings: + * `CFAbstractAnalysis.getFieldValues` => `getFieldInitialValues` + +The following methods no longer take a `fieldValues` parameter: + * `GenericAnnotatedTypeFactory#createFlowAnalysis` + * `CFAnalysis` construtor + * `CFAbstractAnalysis#performAnalysis` + * `CFAbstractAnalysis` constructors + +**Closed issues:** +#4685, #4689, #4785, #4805, #4806, #4815, #4829, #4849. + + +Version 3.16.0 (July 13, 2021) +------------------------------ + +**User-visible changes:** + +You can run the Checker Framework on a JDK 16 JVM. You can pass the `--release +16` command-line argument to the compiler. You may need to add additional +command-line options, such as `--add-opens`; see the Checker Framework manual. +New syntax, such as records and switch expressions, is not yet supported or +type-checked; that will be added in a future release. Thanks to Neil Brown for +the JDK 16 support. + The Lock Checker supports a new type, `@NewObject`, for the result of a constructor invocation. @@ -32,7 +171,7 @@ Method renamings in `AnnotatedTypes` (the old methods were removed): * `expandVarArgsFromTypes` => `expandVarArgsParametersFromTypes` **Closed issues:** - +#3013, #3754, #3791, #3845, #4523, #4767. Version 3.15.0 (June 18, 2021) ---------------------------- diff --git a/docs/checker-framework-webpage.html b/docs/checker-framework-webpage.html index fd4941932b..27e77c182b 100644 --- a/docs/checker-framework-webpage.html +++ b/docs/checker-framework-webpage.html @@ -30,8 +30,8 @@

The Checker Framework

Installation instructions and tutorial.
  • - Download: checker-framework-3.15.0.zip - (18 Jun 2021); + Download: checker-framework-3.21.0.zip + (17 Dec 2021); includes source, platform-independent binary, tests, and documentation.
    Then, see the installation @@ -93,7 +93,7 @@

    The Checker Framework

    the .class file. The tools support both Java 5 declaration annotations and Java 8 type annotations.