diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d0b8acbf71a..cfe6248ff80 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,7 +1,7 @@ # Contributing to the Checker Framework Thank you for contributing to the Checker Framework! This project is a -community effort of [more than 90 +community effort of [more than 110 developers](https://checkerframework.org/manual/#credits), plus countless more people who have contributed bug reports and feature suggestions. We couldn't do it without your help. @@ -19,18 +19,30 @@ bug, and we want to fix it. Please report it. ## Submitting changes -Please see the [pull requests](https://rawgit.com/typetools/checker-framework/master/docs/developer/developer-manual.html#pull-requests) section of the Developer Manual. +Please see the [pull +requests](https://rawgit.com/typetools/checker-framework/master/docs/developer/developer-manual.html#pull-requests) +section of the Developer Manual. + +Submit changes to the annotated JDK at https://github.com/typetools/jdk/pulls . +Annotations for other libraries can be contributed as stub files in this +repository, in a fork of the library in https://github.com/typetools/, or +in the library's own repository. Do you want to contribute to the project, but you are not sure what issue to fix or what feature to add? Use the tool in your daily work, and when -you encounter a limitation that bothers you, fix that one. +you encounter a limitation that bothers you, fix that one. The ["help +wanted"](https://github.com/typetools/checker-framework/issues?q=is%3Aissue+is%3Aopen+label%3A%22help+wanted%22) +label marks issues that require less deep knowledge and may be appropriate +for a newcomer to the codebase. ## License -By contributing, you agree that your contributions will be licensed under the existing [license](LICENSE.txt), usually GPL2 or MIT License. +By contributing, you agree that your contributions will be licensed under the +existing [license](LICENSE.txt), usually GPL2 or MIT License. ## Code of conduct -In interactions with other people, please abide by the [Contributor Covenant](https://www.contributor-covenant.org/version/2/0/code_of_conduct). +When interacting with other people, please abide by the [Contributor +Covenant](https://www.contributor-covenant.org/version/2/0/code_of_conduct). diff --git a/LICENSE.txt b/LICENSE.txt index 8aa899e4c29..70d6a70fe2f 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -6,34 +6,39 @@ Most of the Checker Framework is licensed under the GNU General Public License, version 2 (GPL2), with the classpath exception. The text of this license appears below. This is the same license used for OpenJDK. -A few parts of the Checker Framework have more permissive licenses. - - * The annotations are licensed under the MIT License. (The text of this - license also appears below.) More specifically, all the parts of the - Checker Framework that you might want to include with your own program - use the MIT License. This is the checker-qual*.jar and - checker-compat-qual*.jar files and all the files that appear in them: - every file in a qual/ directory, plus utility files such as - NullnessUtil.java, RegexUtil.java, SignednessUtil.java, etc. In - addition, the cleanroom implementations of third-party annotations, - which the Checker Framework recognizes as aliases for its own - annotations, are licensed under the MIT License. - -Some external libraries that are included with the Checker Framework have -different licenses. +A few parts of the Checker Framework have more permissive licenses, notably +the parts that you might want to include with your own program. + + * The annotations and utility files are licensed under the MIT License. + (The text of this license also appears below.) This applies to the + checker-qual*.jar and all the files that appear in it: every file in a + qual/ directory, plus utility files FormatUtil.java, + I18nFormatUtil.java, NullnessUtil.java, Opt.java, PurityUnqualified.java, + RegexUtil.java, SignednessUtil.java, SignednessUtilExtra.java, and + UnitsTools.java. It also applies to the cleanroom implementations of + third-party annotations (in checker/src/testannotations/ and in + framework/src/main/java/org/jmlspecs/). + +The Checker Framework includes annotations for some libraries. Those in +.astub files use the MIT License. Those in https://github.com/typetools/jdk +(which appears in the annotated-jdk directory of file checker.jar) use the +GPL2 license. + +Some external libraries that are included with the Checker Framework +distribution have different licenses. Here are some examples. * javaparser is dual licensed under the LGPL or the Apache license -- you may use it under whichever one you want. (The javaparser source code contains a file with the text of the GPL, but it is not clear why, since - javaparser does not use the GPL.) See file stubparser/LICENSE - and the source code of all its files. + javaparser does not use the GPL.) See + https://github.com/typetools/stubparser . + + * Annotation Tools (https://github.com/typetools/annotation-tools) uses + the MIT license. * Libraries in plume-lib (https://github.com/plume-lib/) are licensed under the MIT License. -The Checker Framework includes annotations for some libraries. Each annotated -library uses the same license as the unannotated version of the library. - =========================================================================== The GNU General Public License (GPL) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 4101d78127b..a9d5e18e382 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -10,25 +10,55 @@ pr: jobs: -- job: all_tests_jdk8 +- job: junit_tests_jdk8 + dependsOn: + - junit_tests_jdk11 + - nonjunit_tests_jdk11 + - misc_jdk11 pool: vmImage: 'ubuntu-latest' container: mdernst/cf-ubuntu-jdk8:latest steps: - checkout: self fetchDepth: 25 - - bash: ./checker/bin-devel/test-cftests-all.sh - displayName: test-cftests-all.sh -- job: all_tests_jdk11 + - bash: ./checker/bin-devel/test-cftests-junit.sh + displayName: test-cftests-junit.sh +- job: junit_tests_jdk11 pool: vmImage: 'ubuntu-latest' container: mdernst/cf-ubuntu-jdk11:latest steps: - checkout: self fetchDepth: 25 - - bash: ./checker/bin-devel/test-cftests-all.sh - displayName: test-cftests-all.sh + - bash: ./checker/bin-devel/test-cftests-junit.sh + displayName: test-cftests-junit.sh +- job: nonjunit_tests_jdk8 + dependsOn: + - junit_tests_jdk11 + - nonjunit_tests_jdk11 + - misc_jdk11 + pool: + vmImage: 'ubuntu-latest' + container: mdernst/cf-ubuntu-jdk8:latest + steps: + - checkout: self + fetchDepth: 25 + - bash: ./checker/bin-devel/test-cftests-nonjunit.sh + displayName: test-cftests-nonjunit.sh +- job: nonjunit_tests_jdk11 + pool: + vmImage: 'ubuntu-latest' + container: mdernst/cf-ubuntu-jdk11:latest + steps: + - checkout: self + fetchDepth: 25 + - bash: ./checker/bin-devel/test-cftests-nonjunit.sh + displayName: test-cftests-nonjunit.sh - job: misc_jdk8 + dependsOn: + - junit_tests_jdk11 + - nonjunit_tests_jdk11 + - misc_jdk11 pool: vmImage: 'ubuntu-latest' container: mdernst/cf-ubuntu-jdk8-plus:latest @@ -47,6 +77,11 @@ jobs: - bash: ./checker/bin-devel/test-misc.sh displayName: test-misc.sh - job: cf_inference_jdk8 + dependsOn: + - junit_tests_jdk11 + - nonjunit_tests_jdk11 + - misc_jdk11 + - cf_inference_jdk11 pool: vmImage: 'ubuntu-latest' container: mdernst/cf-ubuntu-jdk8:latest @@ -65,6 +100,11 @@ jobs: - bash: ./checker/bin-devel/test-cf-inference.sh displayName: test-cf-inference.sh - job: daikon_jdk8 + dependsOn: + - junit_tests_jdk11 + - nonjunit_tests_jdk11 + - misc_jdk11 + - daikon_jdk11 pool: vmImage: 'ubuntu-latest' container: mdernst/cf-ubuntu-jdk8:latest @@ -83,6 +123,11 @@ jobs: - bash: ./checker/bin-devel/test-daikon.sh displayName: test-daikon.sh - job: guava_jdk8 + dependsOn: + - junit_tests_jdk11 + - nonjunit_tests_jdk11 + - misc_jdk11 + - guava_jdk11 pool: vmImage: 'ubuntu-latest' container: mdernst/cf-ubuntu-jdk8:latest @@ -103,6 +148,11 @@ jobs: - bash: ./checker/bin-devel/test-guava.sh displayName: test-guava.sh - job: plume_lib_jdk8 + dependsOn: + - junit_tests_jdk11 + - nonjunit_tests_jdk11 + - misc_jdk11 + - plume_lib_jdk11 pool: vmImage: 'ubuntu-latest' container: mdernst/cf-ubuntu-jdk8:latest @@ -120,19 +170,24 @@ jobs: fetchDepth: 25 - bash: ./checker/bin-devel/test-plume-lib.sh displayName: test-plume-lib.sh -- job: downstream_jdk11 +- job: downstream_jdk8 + dependsOn: + - junit_tests_jdk11 + - nonjunit_tests_jdk11 + - misc_jdk11 + - downstream_jdk11 pool: vmImage: 'ubuntu-latest' - container: mdernst/cf-ubuntu-jdk11:latest + container: mdernst/cf-ubuntu-jdk8:latest steps: - checkout: self fetchDepth: 25 - bash: ./checker/bin-devel/test-downstream.sh displayName: test-downstream.sh -- job: downstream_jdk8 +- job: downstream_jdk11 pool: vmImage: 'ubuntu-latest' - container: mdernst/cf-ubuntu-jdk8:latest + container: mdernst/cf-ubuntu-jdk11:latest steps: - checkout: self fetchDepth: 25 diff --git a/build.gradle b/build.gradle index 9c25ac6ec20..7ffd2d6c521 100644 --- a/build.gradle +++ b/build.gradle @@ -4,14 +4,14 @@ plugins { // https://plugins.gradle.org/plugin/com.github.johnrengelman.shadow (v5 requires Gradle 5) id 'com.github.johnrengelman.shadow' version '6.0.0' // https://plugins.gradle.org/plugin/de.undercouch.download - id "de.undercouch.download" version "4.0.4" + id "de.undercouch.download" version "4.1.1" id 'java' // https://github.com/tbroyer/gradle-errorprone-plugin id "net.ltgt.errorprone" version "1.2.1" // https://plugins.gradle.org/plugin/org.ajoberstar.grgit id 'org.ajoberstar.grgit' version '4.0.2' apply false // https://github.com/n0mer/gradle-git-properties ; target is: generateGitProperties - id "com.gorylenko.gradle-git-properties" version "2.2.2" + id "com.gorylenko.gradle-git-properties" version "2.2.3" } apply plugin: "de.undercouch.download" @@ -101,7 +101,7 @@ allprojects { // * any new checkers have been added, // * the patch level is 9 (keep the patch level as a single digit), or // * backward-incompatible changes have been made to APIs or elsewhere. - version '3.5.0' + version '3.6.1-SNAPSHOT' repositories { mavenCentral() @@ -220,8 +220,43 @@ allprojects { if (isJava8) { options.forkOptions.jvmArgs += ["-Xbootclasspath/p:${configurations.javacJar.asPath}"] } - // Don't use error-prone by default - options.errorprone.enabled = false + + // Error prone depends on checker-qual.jar, so don't run it on that project to avoid a circular dependency. + // All the classes in checker-qual are also in checker, so they are checked. + // TODO: enable Error Prone on test classes. + if (name.is('compileJava') && !project.name.startsWith('checker-qual')){ + println "${project.name} ${name}" + // Error Prone must be available in the annotation processor path + options.annotationProcessorPath = configurations.errorprone + // Enable Error Prone + options.errorprone.enabled = true + options.errorprone.disableWarningsInGeneratedCode = true + options.errorprone.errorproneArgs = [ + // Many compiler classes are interned. + '-Xep:ReferenceEquality:OFF', + // These might be worth fixing. + '-Xep:DefaultCharset:OFF', + // Not useful to suggest Splitter; maybe clean up. + '-Xep:StringSplitter:OFF', + // Too broad, rejects seemingly-correct code. + '-Xep:EqualsGetClass:OFF', + // Not a real problem + '-Xep:MixedMutabilityReturnType:OFF', + // Don't want to add a dependency to ErrorProne. + '-Xep:AnnotateFormatMethod:OFF', + // Warns for every use of "@checker_framework.manual" + '-Xep:InvalidBlockTag:OFF', + // @SuppressWarnings doesn't work: https://github.com/google/error-prone/issues/1650 + '-Xep:InlineFormatString:OFF', + // -Werror halts the build if Error Prone issues a warning, which ensures that + // the errors get fixed. On the downside, Error Prone (or maybe the compiler?) + // stops as soon as it issues one warning, rather than outputting them all. + // https://github.com/google/error-prone/issues/436 + '-Werror', + ] + } else { + options.errorprone.enabled = false + } } } } @@ -270,6 +305,9 @@ def createCheckTypeTask(projectName, checker, shortName, args = []) { '-processor', "${checker}", '-proc:only', '-Xlint:-processing', + '-Xmaxerrs', '10000', + '-Xmaxwarns', '10000', + '-ArequirePrefixInWarningSuppressions', ] options.compilerArgs += args @@ -305,7 +343,10 @@ List getJavaFilesToFormat(projectName) { } // Collect all java files in tests directory fileTree("${project(projectName).projectDir}/tests").visit { details -> - if (!details.path.contains("nullness-javac-errors") && details.name.endsWith('java')) { + if (!details.path.contains("nullness-javac-errors") + && !details.path.contains("returnsreceiverdelomboked") + && !details.path.contains("build") + && details.name.endsWith('java')) { javaFiles.add(details.file) } } @@ -316,7 +357,7 @@ List getJavaFilesToFormat(projectName) { } } - // Collect all java files in jtreg directory + // Collect all java files in jtregJdk11 directory fileTree("${project(projectName).projectDir}/jtregJdk11").visit { details -> if (!details.path.contains("nullness-javac-errors") && details.name.endsWith('java')) { javaFiles.add(details.file) @@ -524,7 +565,7 @@ subprojects { // https://mvnrepository.com/artifact/com.google.errorprone/error_prone_core // If you update this: // * Temporarily comment out "-Werror" elsewhere in this file - // * Repeatedly run `./gradlew clean runErrorProne` and fix all errors + // * Repeatedly run `./gradlew clean compileJava` and fix all errors // * Uncomment "-Werror" errorprone group: 'com.google.errorprone', name: 'error_prone_core', version: '2.4.0' } @@ -633,12 +674,14 @@ subprojects { } // Add tasks to run various checkers on all the main source sets. - // TODO: fix or suppress all not.interned warnings and remove the suppression here. - createCheckTypeTask(project.name, 'org.checkerframework.checker.interning.InterningChecker', 'Interning', ['-AsuppressWarnings=not.interned']) - createCheckTypeTask(project.name, 'org.checkerframework.checker.nullness.NullnessChecker', 'Nullness') - createCheckTypeTask(project.name, 'org.checkerframework.checker.nullness.NullnessChecker', 'WorkingNullness', ['-AskipUses=com.sun.*', '-AskipDefs=org.checkerframework.checker.*|org.checkerframework.common.*|org.checkerframework.framework.*|org.checkerframework.dataflow.cfg.CFGBuilder']) + // These pass and are run by nonJunitTests. + createCheckTypeTask(project.name, 'org.checkerframework.checker.interning.InterningChecker', 'Interning') + createCheckTypeTask(project.name, 'org.checkerframework.checker.nullness.NullnessChecker', 'NullnessOnlyAnnotatedFor', ['-AskipUses=com.sun.*', '-AuseConservativeDefaultsForUncheckedCode=source']) createCheckTypeTask(project.name, 'org.checkerframework.framework.util.PurityChecker', 'Purity') createCheckTypeTask(project.name, 'org.checkerframework.checker.signature.SignatureChecker', 'Signature') + // These pass on some subprojects, which nonJunitTests runs. TODO: Incrementally add @AnnotatedFor on classes. + createCheckTypeTask(project.name, 'org.checkerframework.checker.nullness.NullnessChecker', 'Nullness', ['-AskipUses=com.sun.*']) + // Add jtregTests to framework and checker modules if (project.name.is('framework') || project.name.is('checker')) { @@ -757,51 +800,9 @@ subprojects { } } - // Create a runErrorProne task. - tasks.create(name: 'runErrorProne', type: JavaCompile, group: 'Verification') { - description 'Run the error-prone compiler on the main sources' - - source = sourceSets.main.java.asFileTree - classpath = sourceSets.main.compileClasspath.asFileTree - destinationDir = new File("${buildDir}", 'errorprone') - - // Error Prone must be available in the annotation processor path - options.annotationProcessorPath = configurations.errorprone - // Enable Error Prone - options.errorprone.enabled = true - options.errorprone.disableWarningsInGeneratedCode = true - options.errorprone.errorproneArgs = [ - // Many compiler classes are interned. - '-Xep:ReferenceEquality:OFF', - // These might be worth fixing. - '-Xep:DefaultCharset:OFF', - // Not useful to suggest Splitter; maybe clean up. - '-Xep:StringSplitter:OFF', - // Too broad, rejects seemingly-correct code. - '-Xep:EqualsGetClass:OFF', - // Not a real problem - '-Xep:MixedMutabilityReturnType:OFF', - // Don't want to add a dependency to ErrorProne. - '-Xep:AnnotateFormatMethod:OFF', - // Warns for every use of "@checker_framework.manual" - '-Xep:InvalidBlockTag:OFF', - // @SuppressWarnings doesn't work: https://github.com/google/error-prone/issues/1650 - '-Xep:InlineFormatString:OFF', - // -Werror halts the build if Error Prone issues a warning, which ensures that - // the errors get fixed. On the downside, Error Prone (or maybe the compiler?) - // stops as soon as it one warning, rather than outputting them all. - // https://github.com/google/error-prone/issues/436 - '-Werror', - ] - } - // Create a nonJunitTests task per project tasks.create(name: 'nonJunitTests', group: 'Verification') { description 'Run all Checker Framework tests except for the Junit tests.' - dependsOn('checkInterning', 'checkPurity', 'checkSignature', 'checkWorkingNullness') - if (project.name.is('framework') || project.name.is('checker')) { - dependsOn('checkCompilerMessages', 'jtregTests') - } if (project.name.is('framework')) { dependsOn('wholeProgramInferenceTests', 'loaderTests') } @@ -815,15 +816,31 @@ subprojects { if (project.name.is('dataflow')) { dependsOn('liveVariableTest') + dependsOn('issue3447Test') + } + } + + // Create a typecheck task per project (dogfooding the Checker Framework on itself). + // This isn't a test of the Checker Framework as the test and nonJunitTests are. + tasks.create(name: 'typecheck', group: 'Verification') { + description 'Run the Checker Framework on itself' + dependsOn('checkInterning', 'checkPurity', 'checkSignature') + if (project.name.is('framework') || project.name.is('checker')) { + dependsOn('checkNullnessOnlyAnnotatedFor') + } else { + dependsOn('checkNullness') + } + if (project.name.is('framework') || project.name.is('checker')) { + dependsOn('checkCompilerMessages', 'jtregTests') } } // Create an allTests task per project. - // allTests = test + nonJunitTests + // allTests = test + nonJunitTests + typecheck tasks.create(name: 'allTests', group: 'Verification') { description 'Run all Checker Framework tests' // The 'test' target is just the JUnit tests. - dependsOn('nonJunitTests', 'test') + dependsOn('nonJunitTests', 'test', 'typecheck') } task javadocPrivate(dependsOn: javadoc) { @@ -845,6 +862,7 @@ task checkBasicStyle(group: 'Format') { '.gradle', '.idea', '.plume-scripts', + '.run-google-java-format', 'annotated', 'api', 'bib', @@ -877,6 +895,8 @@ task checkBasicStyle(group: 'Format') { 'manual.html', 'manual.html-e', 'junit.*.properties', + 'checker/dist/META-INF/maven/org.apache.bcel/bcel/pom.xml', + 'checker/dist/META-INF/maven/org.apache.commons/commons-text/pom.xml', 'framework/src/main/resources/git.properties'] doLast { FileTree tree = fileTree(dir: projectDir) @@ -889,12 +909,19 @@ task checkBasicStyle(group: 'Format') { boolean failed = false tree.visit { if (!it.file.isDirectory()) { - int isBlankLine + boolean blankLineAtEnd = false + String fileName = it.file.getName() + boolean checkTabs = !fileName.equals("Makefile") it.file.eachLine { line -> if (line.endsWith(' ')) { println("Trailing whitespace: ${it.file.absolutePath}") failed = true } + if (checkTabs && line.contains('\t')) { + println("Contains tab (use spaces): ${it.file.absolutePath}") + failed = true + checkTabs = false + } if (!line.startsWith('\\') && (line.matches('^.* (else|finally|try)\\{}.*$') || line.matches('^.*}(catch|else|finally) .*$') @@ -904,13 +931,13 @@ task checkBasicStyle(group: 'Format') { failed = true } if (line.isEmpty()) { - isBlankLine++; + blankLineAtEnd = true; } else { - isBlankLine = 0; + blankLineAtEnd = false; } } - if (isBlankLine > 1) { + if (blankLineAtEnd) { println("Blank line at end of file: ${it.file.absolutePath}") failed = true } diff --git a/changelog.txt b/changelog.txt index a053d6d8121..2b1723d574e 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,3 +1,37 @@ +Version 3.6.0, August 3, 2020 + +The Interning Checker supports method annotations @EqualsMethod and +@CompareToMethod. Place them on methods like equals(), compareTo(), and +compare() to permit certain uses of == on non-interned values. + +Added an overloaded version of NullnessUtil.castNonNull that takes an error message. + +Added a new option `-Aversion` to print the version of the Checker Framework. + +New CFGVisualizeLauncher command-line arguments: + * `--outputdir`: directory in which to write output files + * `--string`: print the control flow graph in the terminal +All CFGVisualizeLauncher command-line arguments now start with `--` instead of `-`. + +Implementation details: + +commonAssignmentCheck() now takes an additional argument. Type system +authors must update their overriding implementations. + +Renamed GenericAnnotatedTypeFactory#addAnnotationsFromDefaultQualifierForUse to +#addAnnotationsFromDefaultForType and +BaseTypeValidator#shouldCheckTopLevelDeclaredType to +#shouldCheckTopLevelDeclaredOrPrimitiveType + +Removed org.checkerframework.framework.test.FrameworkPer(Directory/File)Test classes. +Use CheckerFrameworkPer(Directory/File)Test instead. + +Closed issues: +1395, 2483, 3207, 3223, 3224, 3313, 3381, 3422, 3424, 3428, 3429, 3438, 3442, +3443, 3447, 3449, 3461, 3482, 3485, 3495, 3500, 3528. + +--------------------------------------------------------------------------- + Version 3.5.0, July 1, 2020 Use "allcheckers:" instead of "all:" as a prefix in a warning suppression string. @@ -111,7 +145,7 @@ Renamings: For collection methods with `Object` formal parameter type, such as contains, indexOf, and remove, the annotated JDK now forbids null as an argument. To make the Nullness Checker permit null, pass -`-Astubs=checker.jar/collection-object-parameters-may-be-null.astub`. +`-Astubs=collection-object-parameters-may-be-null.astub`. The argument to @SuppressWarnings can be a substring of a message key that extends at each end to a period or an end of the key. (Previously, any diff --git a/checker-qual-android/build.gradle b/checker-qual-android/build.gradle index 022874e4e35..92eea98ad90 100644 --- a/checker-qual-android/build.gradle +++ b/checker-qual-android/build.gradle @@ -10,6 +10,7 @@ task copySources(type: Copy, dependsOn: ':checker-qual:copySources') { } from files('../checker-qual/src/main') include "**/*.java" + exclude "**/SignednessUtilExtra.java" into file('src/main') // Not read only because "replaceAnnotations" tasks writes to the files. diff --git a/checker-qual/build.gradle b/checker-qual/build.gradle index 121e0c1067c..69c84442fbb 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.1.1" + classpath "biz.aQute.bnd:biz.aQute.bnd.gradle:5.1.2" } } @@ -17,16 +17,19 @@ task copySources(type: Copy) { } from files('../checker/src/main/java', '../dataflow/src/main/java', '../framework/src/main/java') + // Qualifiers + include '**/org/checkerframework/**/qual/*.java' + include '**/PurityUnqualified.java' // TODO: Should we move this into a qual directory? + // Utility classes + // If you change this list, also update ../LICENSE.txt . include "**/FormatUtil.java" + include "**/I18nFormatUtil.java" include "**/NullnessUtil.java" + include "**/Opt.java" include "**/RegexUtil.java" - include "**/UnitsTools.java" include "**/SignednessUtil.java" - include "**/I18nFormatUtil.java" - include '**/org/checkerframework/**/qual/*.java' - include '**/Opt.java' - // TODO: Should we move this into a qual directory? - include '**/PurityUnqualified.java' + include "**/SignednessUtilExtra.java" + include "**/UnitsTools.java" // Make files read only. fileMode(0444) diff --git a/checker/bin-devel/git.pre-commit b/checker/bin-devel/git.pre-commit index 486ad95cc47..b2d1f81505c 100755 --- a/checker/bin-devel/git.pre-commit +++ b/checker/bin-devel/git.pre-commit @@ -12,13 +12,15 @@ 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/' ` || true -# echo CHANGED_JAVA_FILES "'"${CHANGED_JAVA_FILES}"'" -if [ ! -z "$CHANGED_JAVA_FILES" ]; then +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 +# echo "CHANGED_JAVA_FILES=${CHANGED_JAVA_FILES}" +if [ -n "$CHANGED_JAVA_FILES" ]; then ./gradlew getCodeFormatScripts -q ## For debugging: # echo "CHANGED_JAVA_FILES: ${CHANGED_JAVA_FILES}" - python checker/bin-devel/.run-google-java-format/check-google-java-format.py --aosp ${CHANGED_JAVA_FILES} || (echo "Try running: ./gradlew reformat" && /bin/false) + + # shellcheck disable=SC2086 + python checker/bin-devel/.run-google-java-format/check-google-java-format.py --aosp ${CHANGED_JAVA_FILES} || (echo "Problem in pre-commit. Try running: ./gradlew reformat" && /bin/false) BRANCH=$(git rev-parse --abbrev-ref HEAD) if [ "$BRANCH" = "master" ]; then @@ -34,11 +36,14 @@ fi # This is to handle non-.java files, since the above already handled .java files. # May need to remove files that are allowed to have trailing whitespace or are # not text files. -CHANGED_FILES=`git diff --staged --name-only --diff-filter=ACM | grep -v '\.class$' | grep -v '\.gz$' | grep -v '\.jar$' | grep -v '\.pdf$' | grep -v '\.png$' | grep -v '\.xcf$'` || true -if [ ! -z "$CHANGED_FILES" ]; then +CHANGED_FILES=$(git diff --staged --name-only --diff-filter=ACM | grep -v '\.class$' | grep -v '\.gz$' | grep -v '\.jar$' | grep -v '\.pdf$' | grep -v '\.png$' | grep -v '\.xcf$') || true +if [ -n "$CHANGED_FILES" ]; then + ## For debugging: # echo "CHANGED_FILES: ${CHANGED_FILES}" - FILES_WITH_TRAILING_SPACES=`grep -l -s '[[:blank:]]$' ${CHANGED_FILES} 2>&1` || true - if [ ! -z "$FILES_WITH_TRAILING_SPACES" ]; then + + # shellcheck disable=SC2086 + FILES_WITH_TRAILING_SPACES=$(grep -l -s '[[:blank:]]$' ${CHANGED_FILES} 2>&1) || true + if [ -n "$FILES_WITH_TRAILING_SPACES" ]; then echo "Some files have trailing whitespace: ${FILES_WITH_TRAILING_SPACES}" && exit 1 fi fi diff --git a/checker/bin-devel/test-daikon-part1.sh b/checker/bin-devel/test-daikon-part1.sh index 671590df1a6..92991762130 100755 --- a/checker/bin-devel/test-daikon-part1.sh +++ b/checker/bin-devel/test-daikon-part1.sh @@ -19,7 +19,7 @@ git log | head -n 5 make compile if [ "$TRAVIS" = "true" ] ; then # Travis kills a job if it runs 10 minutes without output - time make JAVACHECK_EXTRA_ARGS=-Afilenames -C java check-part1 + time make JAVACHECK_EXTRA_ARGS=-Afilenames -C java typecheck-part1 else - time make -C java check-part1 + time make -C java typecheck-part1 fi diff --git a/checker/bin-devel/test-daikon-part2.sh b/checker/bin-devel/test-daikon-part2.sh index cb2de5bb6bf..e4313823320 100755 --- a/checker/bin-devel/test-daikon-part2.sh +++ b/checker/bin-devel/test-daikon-part2.sh @@ -19,7 +19,7 @@ git log | head -n 5 make compile if [ "$TRAVIS" = "true" ] ; then # Travis kills a job if it runs 10 minutes without output - time make JAVACHECK_EXTRA_ARGS=-Afilenames -C java check-part2 + time make JAVACHECK_EXTRA_ARGS=-Afilenames -C java typecheck-part2 else - time make -C java check-part2 + time make -C java typecheck-part2 fi diff --git a/checker/bin-devel/test-misc.sh b/checker/bin-devel/test-misc.sh index c5fd22fe78e..a93a57713dd 100755 --- a/checker/bin-devel/test-misc.sh +++ b/checker/bin-devel/test-misc.sh @@ -12,16 +12,17 @@ SCRIPTDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" source "$SCRIPTDIR"/build.sh -# Code style and formatting -./gradlew checkBasicStyle checkFormat --console=plain --warning-mode=all --no-daemon - -# Run error-prone -./gradlew runErrorProne --console=plain --warning-mode=all --no-daemon +# Code style, formatting, and plugable type-checking +./gradlew checkBasicStyle checkFormat typecheck --console=plain --warning-mode=all --no-daemon # HTML legality ./gradlew htmlValidate --console=plain --warning-mode=all --no-daemon # Javadoc documentation +# Uncomment this line temporarily for refactorings that touch a lot of code that +# you don't understand. Then, recomment it as soon as the pull request is merged. +# SKIPJAVADOC=1 +if [ -z "$SKIPJAVADOC" ]; then status=0 ./gradlew javadoc --console=plain --warning-mode=all --no-daemon || status=1 ./gradlew javadocPrivate --console=plain --warning-mode=all --no-daemon || status=1 @@ -30,6 +31,8 @@ status=0 (./gradlew javadocDoclintAll --console=plain --warning-mode=all --no-daemon > /tmp/warnings-jda.txt 2>&1) || true /tmp/"$USER"/plume-scripts/ci-lint-diff /tmp/warnings-jda.txt || status=1 if [ $status -ne 0 ]; then exit $status; fi +fi # end of "if [ -z $SKIPJAVADOC ]" + # User documentation make -C docs/manual all diff --git a/checker/jtreg/nullness/defaultsPersist/ReferenceInfoUtil.java b/checker/jtreg/nullness/defaultsPersist/ReferenceInfoUtil.java index 1743208fbc7..9431e984ae7 100644 --- a/checker/jtreg/nullness/defaultsPersist/ReferenceInfoUtil.java +++ b/checker/jtreg/nullness/defaultsPersist/ReferenceInfoUtil.java @@ -15,7 +15,7 @@ import java.util.Arrays; import java.util.List; import org.checkerframework.javacutil.Pair; -import org.checkerframework.javacutil.PluginUtil; +import org.checkerframework.javacutil.SystemUtil; public class ReferenceInfoUtil { @@ -159,7 +159,7 @@ && areEquals(p1.type_index, p2.type_index) public static String positionCompareStr( TypeAnnotation.Position p1, TypeAnnotation.Position p2) { - return PluginUtil.joinLines( + return SystemUtil.joinLines( "type = " + p1.type + ", " + p2.type, "offset = " + p1.offset + ", " + p2.offset, "lvarOffset = " + p1.lvarOffset + ", " + p2.lvarOffset, @@ -234,7 +234,7 @@ public ComparisonException( } public String toString() { - return PluginUtil.joinLines( + return SystemUtil.joinLines( super.toString(), "\tExpected: " + expected.size() diff --git a/checker/jtreg/nullness/inheritDeclAnnoPersist/Extends.java b/checker/jtreg/nullness/inheritDeclAnnoPersist/Extends.java index f895a3027aa..20b905c97b5 100644 --- a/checker/jtreg/nullness/inheritDeclAnnoPersist/Extends.java +++ b/checker/jtreg/nullness/inheritDeclAnnoPersist/Extends.java @@ -1,4 +1,4 @@ -import org.checkerframework.javacutil.PluginUtil; +import org.checkerframework.javacutil.SystemUtil; /* * @test @@ -37,7 +37,7 @@ public String m3() { class TestWrapper { public static String wrap(String... method) { - return PluginUtil.joinLines( - "class Test extends Super {", PluginUtil.joinLines(method), "}"); + return SystemUtil.joinLines( + "class Test extends Super {", SystemUtil.joinLines(method), "}"); } } diff --git a/checker/jtreg/nullness/inheritDeclAnnoPersist/Implements.java b/checker/jtreg/nullness/inheritDeclAnnoPersist/Implements.java index 247916f2b7e..55b5ce89697 100644 --- a/checker/jtreg/nullness/inheritDeclAnnoPersist/Implements.java +++ b/checker/jtreg/nullness/inheritDeclAnnoPersist/Implements.java @@ -1,4 +1,4 @@ -import org.checkerframework.javacutil.PluginUtil; +import org.checkerframework.javacutil.SystemUtil; /* * @test @@ -23,7 +23,7 @@ public String m1() { class TestWrapper { public static String wrap(String... method) { - return PluginUtil.joinLines( - "class Test extends AbstractClass {", PluginUtil.joinLines(method), "}"); + return SystemUtil.joinLines( + "class Test extends AbstractClass {", SystemUtil.joinLines(method), "}"); } } diff --git a/checker/jtreg/nullness/inheritDeclAnnoPersist/ReferenceInfoUtil.java b/checker/jtreg/nullness/inheritDeclAnnoPersist/ReferenceInfoUtil.java index 02c9c8256e0..e3d6172521a 100644 --- a/checker/jtreg/nullness/inheritDeclAnnoPersist/ReferenceInfoUtil.java +++ b/checker/jtreg/nullness/inheritDeclAnnoPersist/ReferenceInfoUtil.java @@ -12,7 +12,7 @@ import java.util.ArrayList; import java.util.List; import java.util.StringJoiner; -import org.checkerframework.javacutil.PluginUtil; +import org.checkerframework.javacutil.SystemUtil; public class ReferenceInfoUtil { @@ -123,7 +123,7 @@ public String toString() { throw new RuntimeException(e); } } - return PluginUtil.joinLines( + return SystemUtil.joinLines( super.toString(), "\tExpected: " + expected.size() diff --git a/checker/jtreg/sortwarnings/ErrorOrders.out b/checker/jtreg/sortwarnings/ErrorOrders.out index ceddf61d07b..87f467eac8e 100644 --- a/checker/jtreg/sortwarnings/ErrorOrders.out +++ b/checker/jtreg/sortwarnings/ErrorOrders.out @@ -26,46 +26,46 @@ ErrorOrders.java:24:47: compiler.err.proc.messager: [expression.unparsable.type. ErrorOrders.java:24:55: compiler.err.proc.messager: [assignment.type.incompatible] incompatible types in assignment. found : @LTLengthOf(value="p2") int required: @LTLengthOf(value="[error for expression: This isn't an expression; error: Invalid 'This isn't an expression' because is an invalid expression]") int -ErrorOrders.java:29:15: compiler.err.proc.messager: [argument.type.incompatible] incompatible types in argument. +ErrorOrders.java:29:15: compiler.err.proc.messager: [argument.type.incompatible] incompatible argument for parameter p1 of test4. found : @LowerBoundUnknown int required: @GTENegativeOne int -ErrorOrders.java:29:15: compiler.err.proc.messager: [argument.type.incompatible] incompatible types in argument. +ErrorOrders.java:29:15: compiler.err.proc.messager: [argument.type.incompatible] incompatible argument for parameter p1 of test4. found : @UpperBoundUnknown int required: @UpperBoundBottom int -ErrorOrders.java:29:24: compiler.err.proc.messager: [argument.type.incompatible] incompatible types in argument. +ErrorOrders.java:29:24: compiler.err.proc.messager: [argument.type.incompatible] incompatible argument for parameter p2 of test4. found : @LowerBoundUnknown int required: @GTENegativeOne int -ErrorOrders.java:29:24: compiler.err.proc.messager: [argument.type.incompatible] incompatible types in argument. +ErrorOrders.java:29:24: compiler.err.proc.messager: [argument.type.incompatible] incompatible argument for parameter p2 of test4. found : @UpperBoundUnknown int required: @UpperBoundBottom int -ErrorOrders.java:29:25: compiler.err.proc.messager: [argument.type.incompatible] incompatible types in argument. +ErrorOrders.java:29:25: compiler.err.proc.messager: [argument.type.incompatible] incompatible argument for parameter p1 of test4. found : @LowerBoundUnknown int required: @GTENegativeOne int -ErrorOrders.java:29:25: compiler.err.proc.messager: [argument.type.incompatible] incompatible types in argument. +ErrorOrders.java:29:25: compiler.err.proc.messager: [argument.type.incompatible] incompatible argument for parameter p1 of test4. found : @UpperBoundUnknown int required: @UpperBoundBottom int -ErrorOrders.java:29:29: compiler.err.proc.messager: [argument.type.incompatible] incompatible types in argument. +ErrorOrders.java:29:29: compiler.err.proc.messager: [argument.type.incompatible] incompatible argument for parameter p2 of test4. found : @LowerBoundUnknown int required: @GTENegativeOne int -ErrorOrders.java:29:29: compiler.err.proc.messager: [argument.type.incompatible] incompatible types in argument. +ErrorOrders.java:29:29: compiler.err.proc.messager: [argument.type.incompatible] incompatible argument for parameter p2 of test4. found : @UpperBoundUnknown int required: @UpperBoundBottom int -ErrorOrders.java:29:33: compiler.err.proc.messager: [argument.type.incompatible] incompatible types in argument. +ErrorOrders.java:29:33: compiler.err.proc.messager: [argument.type.incompatible] incompatible argument for parameter p3 of test4. found : @UnknownVal int @UnknownVal [] required: @UnknownVal int [] -ErrorOrders.java:29:37: compiler.err.proc.messager: [argument.type.incompatible] incompatible types in argument. +ErrorOrders.java:29:37: compiler.err.proc.messager: [argument.type.incompatible] incompatible argument for parameter p4 of test4. found : @SameLenUnknown int @SameLen("p4") [] required: @SameLenUnknown int @SameLenBottom [] -ErrorOrders.java:29:41: compiler.err.proc.messager: [argument.type.incompatible] incompatible types in argument. +ErrorOrders.java:29:41: compiler.err.proc.messager: [argument.type.incompatible] incompatible argument for parameter p5 of test4. found : @UnknownVal int required: int -ErrorOrders.java:29:46: compiler.err.proc.messager: [argument.type.incompatible] incompatible types in argument. +ErrorOrders.java:29:46: compiler.err.proc.messager: [argument.type.incompatible] incompatible argument for parameter p3 of test4. found : @UnknownVal int @UnknownVal [] required: @UnknownVal int [] -ErrorOrders.java:29:50: compiler.err.proc.messager: [argument.type.incompatible] incompatible types in argument. +ErrorOrders.java:29:50: compiler.err.proc.messager: [argument.type.incompatible] incompatible argument for parameter p4 of test4. found : @SameLenUnknown int @SameLen("p4") [] required: @SameLenUnknown int @SameLenBottom [] -ErrorOrders.java:29:54: compiler.err.proc.messager: [argument.type.incompatible] incompatible types in argument. +ErrorOrders.java:29:54: compiler.err.proc.messager: [argument.type.incompatible] incompatible argument for parameter p5 of test4. found : @UnknownVal int required: int ErrorOrders.java:33:47: compiler.err.proc.messager: [expression.unparsable.type.invalid] Expression invalid in dependent type annotation: [error for expression: This isn't an expression; error: Invalid 'This isn't an expression' because is an invalid expression] @@ -95,46 +95,46 @@ ErrorOrders.java:56:47: compiler.err.proc.messager: [expression.unparsable.type. ErrorOrders.java:56:55: compiler.err.proc.messager: [assignment.type.incompatible] incompatible types in assignment. found : @LTLengthOf(value="p2") int required: @LTLengthOf(value="[error for expression: This isn't an expression; error: Invalid 'This isn't an expression' because is an invalid expression]") int -ErrorOrders.java:61:15: compiler.err.proc.messager: [argument.type.incompatible] incompatible types in argument. +ErrorOrders.java:61:15: compiler.err.proc.messager: [argument.type.incompatible] incompatible argument for parameter p1 of test4. found : @LowerBoundUnknown int required: @GTENegativeOne int -ErrorOrders.java:61:15: compiler.err.proc.messager: [argument.type.incompatible] incompatible types in argument. +ErrorOrders.java:61:15: compiler.err.proc.messager: [argument.type.incompatible] incompatible argument for parameter p1 of test4. found : @UpperBoundUnknown int required: @UpperBoundBottom int -ErrorOrders.java:61:24: compiler.err.proc.messager: [argument.type.incompatible] incompatible types in argument. +ErrorOrders.java:61:24: compiler.err.proc.messager: [argument.type.incompatible] incompatible argument for parameter p2 of test4. found : @LowerBoundUnknown int required: @GTENegativeOne int -ErrorOrders.java:61:24: compiler.err.proc.messager: [argument.type.incompatible] incompatible types in argument. +ErrorOrders.java:61:24: compiler.err.proc.messager: [argument.type.incompatible] incompatible argument for parameter p2 of test4. found : @UpperBoundUnknown int required: @UpperBoundBottom int -ErrorOrders.java:61:25: compiler.err.proc.messager: [argument.type.incompatible] incompatible types in argument. +ErrorOrders.java:61:25: compiler.err.proc.messager: [argument.type.incompatible] incompatible argument for parameter p1 of test4. found : @LowerBoundUnknown int required: @GTENegativeOne int -ErrorOrders.java:61:25: compiler.err.proc.messager: [argument.type.incompatible] incompatible types in argument. +ErrorOrders.java:61:25: compiler.err.proc.messager: [argument.type.incompatible] incompatible argument for parameter p1 of test4. found : @UpperBoundUnknown int required: @UpperBoundBottom int -ErrorOrders.java:61:29: compiler.err.proc.messager: [argument.type.incompatible] incompatible types in argument. +ErrorOrders.java:61:29: compiler.err.proc.messager: [argument.type.incompatible] incompatible argument for parameter p2 of test4. found : @LowerBoundUnknown int required: @GTENegativeOne int -ErrorOrders.java:61:29: compiler.err.proc.messager: [argument.type.incompatible] incompatible types in argument. +ErrorOrders.java:61:29: compiler.err.proc.messager: [argument.type.incompatible] incompatible argument for parameter p2 of test4. found : @UpperBoundUnknown int required: @UpperBoundBottom int -ErrorOrders.java:61:33: compiler.err.proc.messager: [argument.type.incompatible] incompatible types in argument. +ErrorOrders.java:61:33: compiler.err.proc.messager: [argument.type.incompatible] incompatible argument for parameter p3 of test4. found : @UnknownVal int @UnknownVal [] required: @UnknownVal int [] -ErrorOrders.java:61:37: compiler.err.proc.messager: [argument.type.incompatible] incompatible types in argument. +ErrorOrders.java:61:37: compiler.err.proc.messager: [argument.type.incompatible] incompatible argument for parameter p4 of test4. found : @SameLenUnknown int @SameLen("p4") [] required: @SameLenUnknown int @SameLenBottom [] -ErrorOrders.java:61:41: compiler.err.proc.messager: [argument.type.incompatible] incompatible types in argument. +ErrorOrders.java:61:41: compiler.err.proc.messager: [argument.type.incompatible] incompatible argument for parameter p5 of test4. found : @UnknownVal int required: int -ErrorOrders.java:61:46: compiler.err.proc.messager: [argument.type.incompatible] incompatible types in argument. +ErrorOrders.java:61:46: compiler.err.proc.messager: [argument.type.incompatible] incompatible argument for parameter p3 of test4. found : @UnknownVal int @UnknownVal [] required: @UnknownVal int [] -ErrorOrders.java:61:50: compiler.err.proc.messager: [argument.type.incompatible] incompatible types in argument. +ErrorOrders.java:61:50: compiler.err.proc.messager: [argument.type.incompatible] incompatible argument for parameter p4 of test4. found : @SameLenUnknown int @SameLen("p4") [] required: @SameLenUnknown int @SameLenBottom [] -ErrorOrders.java:61:54: compiler.err.proc.messager: [argument.type.incompatible] incompatible types in argument. +ErrorOrders.java:61:54: compiler.err.proc.messager: [argument.type.incompatible] incompatible argument for parameter p5 of test4. found : @UnknownVal int required: int 49 errors diff --git a/checker/jtreg/stubs/issue2147/WithStub.out b/checker/jtreg/stubs/issue2147/WithStub.out index f9e01800442..bb3dd68ac92 100644 --- a/checker/jtreg/stubs/issue2147/WithStub.out +++ b/checker/jtreg/stubs/issue2147/WithStub.out @@ -1,4 +1,4 @@ -EnumStubTest.java:16:31: compiler.err.proc.messager: [argument.type.incompatible] incompatible types in argument. +EnumStubTest.java:16:31: compiler.err.proc.messager: [argument.type.incompatible] incompatible argument for parameter sEnum of requireEnum. found : @Tainted SampleEnum required: @Untainted SampleEnum 1 error diff --git a/checker/jtreg/stubs/issue2147/WithoutStub.out b/checker/jtreg/stubs/issue2147/WithoutStub.out index d543064e481..2f01ee07da0 100644 --- a/checker/jtreg/stubs/issue2147/WithoutStub.out +++ b/checker/jtreg/stubs/issue2147/WithoutStub.out @@ -1,7 +1,7 @@ -EnumStubTest.java:15:31: compiler.err.proc.messager: [argument.type.incompatible] incompatible types in argument. +EnumStubTest.java:15:31: compiler.err.proc.messager: [argument.type.incompatible] incompatible argument for parameter sEnum of requireEnum. found : @Tainted SampleEnum required: @Untainted SampleEnum -EnumStubTest.java:16:31: compiler.err.proc.messager: [argument.type.incompatible] incompatible types in argument. +EnumStubTest.java:16:31: compiler.err.proc.messager: [argument.type.incompatible] incompatible argument for parameter sEnum of requireEnum. found : @Tainted SampleEnum required: @Untainted SampleEnum 2 errors diff --git a/checker/jtreg/tainting/NewClass.out b/checker/jtreg/tainting/NewClass.out index b589c23b8ae..360dc13cf8f 100644 --- a/checker/jtreg/tainting/NewClass.out +++ b/checker/jtreg/tainting/NewClass.out @@ -1,4 +1,4 @@ -NewClass.java:18:49: compiler.err.proc.messager: [argument.type.incompatible] incompatible types in argument. +NewClass.java:18:49: compiler.err.proc.messager: [argument.type.incompatible] incompatible argument for parameter o of get. found : @Tainted Object required: @Untainted Object 1 error diff --git a/checker/src/main/java/org/checkerframework/checker/formatter/FormatUtil.java b/checker/src/main/java/org/checkerframework/checker/formatter/FormatUtil.java index 0205c852ef6..2185c4fa90a 100644 --- a/checker/src/main/java/org/checkerframework/checker/formatter/FormatUtil.java +++ b/checker/src/main/java/org/checkerframework/checker/formatter/FormatUtil.java @@ -10,22 +10,48 @@ import java.util.regex.Pattern; import org.checkerframework.checker.formatter.qual.ConversionCategory; import org.checkerframework.checker.formatter.qual.ReturnsFormat; +import org.checkerframework.checker.regex.qual.Regex; +import org.checkerframework.framework.qual.AnnotatedFor; /** This class provides a collection of utilities to ease working with format strings. */ +@AnnotatedFor("nullness") public class FormatUtil { + + /** + * A representation of a format specifier, which is represented by "%..." in the format string. + * Indicates how to convert a value into a string. + */ private static class Conversion { + /** The index in the argument list. */ private final int index; + /** The conversion category. */ private final ConversionCategory cath; + /** + * Construct a new Conversion. + * + * @param index the index in the argument list + * @param c the conversion character + */ public Conversion(char c, int index) { this.index = index; this.cath = ConversionCategory.fromConversionChar(c); } + /** + * Returns the index in the argument list. + * + * @return the index in the argument list + */ int index() { return index; } + /** + * Returns the conversion category. + * + * @return the conversion category + */ ConversionCategory category() { return cath; } @@ -35,8 +61,14 @@ ConversionCategory category() { * Returns if the format string is satisfiable, and if the format's parameters match the passed * {@link ConversionCategory}s. Otherwise an {@link Error} is thrown. * - *

TODO introduce more such functions, see RegexUtil for examples + * @param format a format string + * @param cc an array of conversion categories + * @return the {@code format} argument + * @throws IllegalFormatException if the format string is incompatible with the conversion + * categories */ + // TODO introduce more such functions, see RegexUtil for examples + @SuppressWarnings("nullness:argument.type.incompatible") // https://tinyurl.com/cfissue/3449 @ReturnsFormat public static String asFormat(String format, ConversionCategory... cc) throws IllegalFormatException { @@ -54,9 +86,19 @@ public static String asFormat(String format, ConversionCategory... cc) return format; } - /** Throws an exception if the format is not syntactically valid. */ + /** + * Throws an exception if the format is not syntactically valid. + * + * @param format a format string + * @throws IllegalFormatException if the format string is invalid + */ + @SuppressWarnings("nullness:argument.type.incompatible") // https://tinyurl.com/cfissue/3449 public static void tryFormatSatisfiability(String format) throws IllegalFormatException { - @SuppressWarnings("unused") + @SuppressWarnings({ + "unused", // called for side effect, to see if it throws an exception + "nullness:argument.type.incompatible" // it's not documented, but String.format permits + // a null array, which it treats as matching any format string + }) String unused = String.format(format, (Object[]) null); } @@ -90,33 +132,69 @@ public static ConversionCategory[] formatParameterCategories(String format) break; } maxindex = Math.max(maxindex, last); + Integer lastKey = last; conv.put( last, ConversionCategory.intersect( - conv.containsKey(last) ? conv.get(last) : ConversionCategory.UNUSED, + conv.containsKey(lastKey) + ? conv.get(lastKey) + : ConversionCategory.UNUSED, c.category())); } ConversionCategory[] res = new ConversionCategory[maxindex + 1]; for (int i = 0; i <= maxindex; ++i) { - res[i] = conv.containsKey(i) ? conv.get(i) : ConversionCategory.UNUSED; + Integer key = i; // autoboxing prevents recognizing that containsKey => get() != null + res[i] = conv.containsKey(key) ? conv.get(key) : ConversionCategory.UNUSED; } return res; } - // %[argument_index$][flags][width][.precision][t]conversion - private static final String formatSpecifier = + /** + * A regex that matches a format specifier. Its syntax is specified in the See {@code + * Formatter} documentation. + * + *

+     * %[argument_index$][flags][width][.precision][t]conversion
+     * group 1            2      3      4           5 6
+     * 
+ * + * For dates and times, the [t] is required and precision must not be provided. For types other + * than dates and times, the [t] must not be provided. + */ + private static final @Regex(6) String formatSpecifier = "%(\\d+\\$)?([-#+ 0,(\\<]*)?(\\d+)?(\\.\\d+)?([tT])?([a-zA-Z%])"; + /** The capturing group for the optional {@code t} character. */ + private static final int formatSpecifierT = 5; + /** + * The capturing group for the last character in a format specifier, which is the conversion + * character unless the {@code t} character was given. + */ + private static final int formatSpecifierConversion = 6; - private static Pattern fsPattern = Pattern.compile(formatSpecifier); + /** + * A Pattern that matches a format specifier. + * + * @see #formatSpecifier + */ + private static @Regex(6) Pattern fsPattern = Pattern.compile(formatSpecifier); + /** + * Return the index, in the argument list, of the value that will be formatted by the matched + * format specifier. + * + * @param m a matcher that matches a format specifier + * @return the index of the argument to format + */ private static int indexFromFormat(Matcher m) { int index; String s = m.group(1); if (s != null) { // explicit index index = Integer.parseInt(s.substring(0, s.length() - 1)); } else { - if (m.group(2) != null && m.group(2).contains(String.valueOf('<'))) { + String group2 = m.group(2); // not @Deterministic, so extract into local var + if (group2 != null && group2.contains(String.valueOf('<'))) { index = -1; // relative index } else { index = 0; // ordinary index @@ -125,18 +203,49 @@ private static int indexFromFormat(Matcher m) { return index; } - private static char conversionCharFromFormat(Matcher m) { - String dt = m.group(5); - if (dt == null) { - return m.group(6).charAt(0); + /** + * Returns the conversion character from a format specifier.. + * + * @param m a matcher that matches a format specifier + * @return the conversion character from the format specifier + */ + @SuppressWarnings( + "nullness:dereference.of.nullable") // group formatSpecifierConversion always exists + private static char conversionCharFromFormat(@Regex(6) Matcher m) { + String tGroup = m.group(formatSpecifierT); + if (tGroup != null) { + return tGroup.charAt(0); // This is the letter "t" or "T". } else { - return dt.charAt(0); + return m.group(formatSpecifierConversion).charAt(0); } } + /** + * Return the conversion character that is in the given format specifier. + * + * @param formatSpecifier a format + * specifier + * @return the conversion character that is in the given format specifier + * @deprecated This method is public only for testing. Use {@link + * #conversionCharFromFormat(Matcher)}. + */ + @Deprecated // used only for testing. Use conversionCharFromFormat(Matcher). + public static char conversionCharFromFormat(String formatSpecifier) { + Matcher m = fsPattern.matcher(formatSpecifier); + assert m.find(); + return conversionCharFromFormat(m); + } + + /** + * Parse the given format string, return information about its format specifiers. + * + * @param format a format string + * @return the list of Conversions from the format specifiers in the format string + */ private static Conversion[] parse(String format) { ArrayList cs = new ArrayList<>(); - Matcher m = fsPattern.matcher(format); + @Regex(7) Matcher m = fsPattern.matcher(format); while (m.find()) { char c = conversionCharFromFormat(m); switch (c) { @@ -194,7 +303,9 @@ public static class IllegalFormatConversionCategoryException public IllegalFormatConversionCategoryException( ConversionCategory expected, ConversionCategory found) { super( - expected.chars.length() == 0 ? '-' : expected.chars.charAt(0), + expected.chars == null || expected.chars.length() == 0 + ? '-' + : expected.chars.charAt(0), found.types == null ? Object.class : found.types[0]); this.expected = expected; this.found = found; 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 fe6b1da3084..5bd7937deb8 100644 --- a/checker/src/main/java/org/checkerframework/checker/formatter/FormatterTreeUtil.java +++ b/checker/src/main/java/org/checkerframework/checker/formatter/FormatterTreeUtil.java @@ -351,6 +351,8 @@ public Boolean visitNull(NullType t, Class p) { } } + // The failure() method is required so that FormatterTransfer, which has no access to the + // FormatterChecker, can report errors. /** * Reports an error. * 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 47d31c067b5..01d6b478c4a 100644 --- a/checker/src/main/java/org/checkerframework/checker/formatter/FormatterVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/formatter/FormatterVisitor.java @@ -9,6 +9,7 @@ import java.util.List; import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.Name; import javax.lang.model.element.VariableElement; import javax.lang.model.type.TypeMirror; import org.checkerframework.checker.compilermsgs.qual.CompilerMessageKey; @@ -88,9 +89,14 @@ public Void visitMethodInvocation(MethodInvocationTree node, Void p) { default: if (!fc.isValidParameter(formatCat, paramType)) { // II.3 + ExecutableElement method = + TreeUtils.elementFromUse(node); + Name methodName = method.getSimpleName(); tu.failure( param, "argument.type.incompatible", + "", // parameter name is not useful + methodName, paramType, formatCat); } @@ -122,10 +128,15 @@ public Void visitMethodInvocation(MethodInvocationTree node, Void p) { } /** - * Returns true if fc is within a method m annotated as {@code @FormatMethod}, and fc's + * Returns true if {@code fc} is within a method m annotated as {@code @FormatMethod}, and fc's * arguments are m's formal parameters. In other words, fc forwards m's arguments to another * format method. + * + * @param fc an invocation of a format method + * @return true if {@code fc} is a call to a format method that forwards its containing methods' + * arguments */ + @SuppressWarnings("interning:not.interned") // comparisons of Name objects private boolean isWrappedFormatCall(FormatCall fc) { MethodTree enclosingMethod = TreeUtils.enclosingMethod(atypeFactory.getPath(fc.node)); @@ -171,8 +182,9 @@ protected void commonAssignmentCheck( AnnotatedTypeMirror varType, AnnotatedTypeMirror valueType, Tree valueTree, - @CompilerMessageKey String errorKey) { - super.commonAssignmentCheck(varType, valueType, valueTree, errorKey); + @CompilerMessageKey String errorKey, + Object... extraArgs) { + super.commonAssignmentCheck(varType, valueType, valueTree, errorKey, extraArgs); AnnotationMirror rhs = valueType.getAnnotationInHierarchy(atypeFactory.UNKNOWNFORMAT); AnnotationMirror lhs = varType.getAnnotationInHierarchy(atypeFactory.UNKNOWNFORMAT); diff --git a/checker/src/main/java/org/checkerframework/checker/formatter/qual/ConversionCategory.java b/checker/src/main/java/org/checkerframework/checker/formatter/qual/ConversionCategory.java index 5c0871a6e02..e64c5f3a801 100644 --- a/checker/src/main/java/org/checkerframework/checker/formatter/qual/ConversionCategory.java +++ b/checker/src/main/java/org/checkerframework/checker/formatter/qual/ConversionCategory.java @@ -7,7 +7,9 @@ import java.util.Date; import java.util.HashSet; import java.util.Set; +import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.dataflow.qual.Pure; +import org.checkerframework.framework.qual.AnnotatedFor; /** * Elements of this enumeration are used in a {@link Format Format} annotation to indicate the valid @@ -28,6 +30,7 @@ * @see Format * @checker_framework.manual #formatter-checker Format String Checker */ +@AnnotatedFor("nullness") public enum ConversionCategory { /** Use if the parameter can be of any type. Applicable for conversions b, B, h, H, s, S. */ GENERAL(null /* everything */, "bBhHsS"), @@ -108,29 +111,29 @@ public enum ConversionCategory { UNUSED(null /* everything */, null); /** Create a new conversion category. */ - ConversionCategory(Class[] types, String chars) { + ConversionCategory(Class @Nullable [] types, @Nullable String chars) { this.types = types; this.chars = chars; } /** The format types. */ @SuppressWarnings("ImmutableEnumChecker") // TODO: clean this up! - public final Class[] types; + public final Class @Nullable [] types; /** The format characters. */ - public final String chars; + public final @Nullable String chars; /** - * Use this function to get the category associated with a conversion character. For example: + * Converts a conversion character to a category. For example: * - *
+ *
{@code
+     * ConversionCategory.fromConversionChar('d') == ConversionCategory.INT
+     * }
* - *
-     * ConversionCategory.fromConversionChar('d') == ConversionCategory.INT;
-     * 
- * - *
+ * @param c a conversion character + * @return the category for the given conversion character */ + @SuppressWarnings("nullness:dereference.of.nullable") // `chars` field is non-null for these public static ConversionCategory fromConversionChar(char c) { for (ConversionCategory v : new ConversionCategory[] {GENERAL, CHAR, INT, FLOAT, TIME}) { if (v.chars.contains(String.valueOf(c))) { @@ -177,14 +180,23 @@ public static ConversionCategory intersect(ConversionCategory a, ConversionCateg return a; } - Set> as = arrayToSet(a.types); - Set> bs = arrayToSet(b.types); + @SuppressWarnings( + "nullness:argument.type.incompatible") // `types` field is null only for UNUSED and + // GENERAL + Set> as = arrayToSet(a.types); + @SuppressWarnings( + "nullness:argument.type.incompatible") // `types` field is null only for UNUSED and + // GENERAL + Set> bs = arrayToSet(b.types); as.retainAll(bs); // intersection for (ConversionCategory v : new ConversionCategory[] { CHAR, INT, FLOAT, TIME, CHAR_AND_INT, INT_AND_TIME, NULL }) { - Set> vs = arrayToSet(v.types); + @SuppressWarnings( + "nullness:argument.type.incompatible") // `types` field is null only for UNUSED + // and GENERAL + Set> vs = arrayToSet(v.types); if (vs.equals(as)) { return v; } @@ -221,14 +233,23 @@ public static ConversionCategory union(ConversionCategory a, ConversionCategory return INT; } - Set> as = arrayToSet(a.types); - Set> bs = arrayToSet(b.types); + @SuppressWarnings( + "nullness:argument.type.incompatible") // `types` field is null only for UNUSED and + // GENERAL + Set> as = arrayToSet(a.types); + @SuppressWarnings( + "nullness:argument.type.incompatible") // `types` field is null only for UNUSED and + // GENERAL + Set> bs = arrayToSet(b.types); as.addAll(bs); // union for (ConversionCategory v : new ConversionCategory[] { NULL, CHAR_AND_INT, INT_AND_TIME, CHAR, INT, FLOAT, TIME }) { - Set> vs = arrayToSet(v.types); + @SuppressWarnings( + "nullness:argument.type.incompatible") // `types` field is null only for UNUSED + // and GENERAL + Set> vs = arrayToSet(v.types); if (vs.equals(as)) { return v; } @@ -266,20 +287,25 @@ private String className(Class cls) { } /** Returns a pretty printed {@link ConversionCategory}. */ + @SuppressWarnings( + "nullness:iterating.over.nullable") // `types` field is null only for UNUSED and + // GENERAL @Pure @Override public String toString() { StringBuilder sb = new StringBuilder(this.name()); - sb.append(" conversion category (one of: "); - boolean first = true; - for (Class cls : this.types) { - if (!first) { - sb.append(", "); + if (this != UNUSED && this != GENERAL) { + sb.append(" conversion category (one of: "); + boolean first = true; + for (Class cls : this.types) { + if (!first) { + sb.append(", "); + } + sb.append(className(cls)); + first = false; } - sb.append(className(cls)); - first = false; + sb.append(")"); } - sb.append(")"); return sb.toString(); } } 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 0da2b746852..3e1feb0e793 100644 --- a/checker/src/main/java/org/checkerframework/checker/guieffect/GuiEffectVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/guieffect/GuiEffectVisitor.java @@ -18,7 +18,9 @@ import java.util.Set; import javax.lang.model.element.AnnotationMirror; 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.DeclaredType; import org.checkerframework.checker.guieffect.qual.AlwaysSafe; import org.checkerframework.checker.guieffect.qual.PolyUI; @@ -210,6 +212,7 @@ public boolean isValidUse( } @Override + @SuppressWarnings("interning:not.interned") // comparing AST nodes public Void visitLambdaExpression(LambdaExpressionTree node, Void p) { Void v = super.visitLambdaExpression(node, p); // If this is a lambda inferred to be @UI, scan up the path and re-check any assignments @@ -435,6 +438,7 @@ public Void visitMethod(MethodTree node, Void p) { } @Override + @SuppressWarnings("interning:not.interned") // comparing AST nodes public Void visitNewClass(NewClassTree node, Void p) { Void v = super.visitNewClass(node, p); // If this is an anonymous inner class inferred to be @UI, scan up the path and re-check any @@ -453,11 +457,13 @@ public Void visitNewClass(NewClassTree node, Void p) { /** * This method is called to traverse the path back up from any anonymous inner class or lambda - * which has been inferred to be UI affecting and re-run {@link #commonAssignmentCheck(Tree, - * ExpressionTree, String)} as needed on places where the class declaration or lambda expression - * are being assigned to a variable, passed as a parameter or returned from a method. This is - * necessary because the normal visitor traversal only checks assignments on the way down the - * AST, before inference has had a chance to run. + * which has been inferred to be UI affecting and re-run {@code commonAssignmentCheck} as needed + * on places where the class declaration or lambda expression are being assigned to a variable, + * passed as a parameter or returned from a method. This is necessary because the normal visitor + * traversal only checks assignments on the way down the AST, before inference has had a chance + * to run. + * + * @param path the path to traverse up from a UI-affecting class */ private void scanUp(TreePath path) { Tree tree = path.getLeaf(); @@ -483,6 +489,9 @@ private void scanUp(TreePath path) { List args = invocationTree.getArguments(); ParameterizedExecutableType mType = atypeFactory.methodFromUse(invocationTree); AnnotatedExecutableType invokedMethod = mType.executableType; + ExecutableElement method = invokedMethod.getElement(); + Name methodName = method.getSimpleName(); + List methodParams = method.getParameters(); List argsTypes = AnnotatedTypes.expandVarArgs( atypeFactory, invokedMethod, invocationTree.getArguments()); @@ -493,7 +502,9 @@ private void scanUp(TreePath path) { argsTypes.get(i), atypeFactory.getAnnotatedType(args.get(i)), args.get(i), - "argument.type.incompatible"); + "argument.type.incompatible", + methodParams.get(i), + methodName); } } break; diff --git a/checker/src/main/java/org/checkerframework/checker/i18nformatter/I18nFormatUtil.java b/checker/src/main/java/org/checkerframework/checker/i18nformatter/I18nFormatUtil.java index 05d80036c81..5e537931178 100644 --- a/checker/src/main/java/org/checkerframework/checker/i18nformatter/I18nFormatUtil.java +++ b/checker/src/main/java/org/checkerframework/checker/i18nformatter/I18nFormatUtil.java @@ -14,12 +14,19 @@ import org.checkerframework.checker.i18nformatter.qual.I18nChecksFormat; import org.checkerframework.checker.i18nformatter.qual.I18nConversionCategory; import org.checkerframework.checker.i18nformatter.qual.I18nValidFormat; +import org.checkerframework.checker.interning.qual.InternedDistinct; +import org.checkerframework.checker.nullness.qual.EnsuresNonNull; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.checker.nullness.qual.RequiresNonNull; +import org.checkerframework.framework.qual.AnnotatedFor; /** * This class provides a collection of utilities to ease working with i18n format strings. * * @checker_framework.manual #i18n-formatter-checker Internationalization Format String Checker */ +@AnnotatedFor("nullness") public class I18nFormatUtil { /** @@ -27,6 +34,9 @@ public class I18nFormatUtil { * * @param format the format string to parse */ + @SuppressWarnings( + "nullness:argument.type.incompatible") // It's not documented, but passing null as the + // argument array is supported. public static void tryFormatSatisfiability(String format) throws IllegalFormatException { MessageFormat.format(format, (Object[]) null); } @@ -47,19 +57,22 @@ public static I18nConversionCategory[] formatParameterCategories(String format) for (I18nConversion c : cs) { int index = c.index; + Integer indexKey = index; conv.put( - index, + indexKey, I18nConversionCategory.intersect( c.category, - conv.containsKey(index) - ? conv.get(index) + conv.containsKey(indexKey) + ? conv.get(indexKey) : I18nConversionCategory.UNUSED)); maxIndex = Math.max(maxIndex, index); } I18nConversionCategory[] res = new I18nConversionCategory[maxIndex + 1]; for (int i = 0; i <= maxIndex; i++) { - res[i] = conv.containsKey(i) ? conv.get(i) : I18nConversionCategory.UNUSED; + Integer indexKey = i; + res[i] = + conv.containsKey(indexKey) ? conv.get(indexKey) : I18nConversionCategory.UNUSED; } return res; } @@ -117,18 +130,22 @@ private static class MessageFormatParser { public static int maxOffset; - /** The locale to use for formatting numbers and dates. */ - private static Locale locale; + /** The locale to use for formatting numbers and dates. Is set in {@link #parse}. */ + private static @MonotonicNonNull Locale locale; - /** An array of formatters, which are used to format the arguments. */ - private static List categories; + /** + * An array of formatters, which are used to format the arguments. Is set in {@link #parse}. + */ + private static @MonotonicNonNull List categories; /** * The argument numbers corresponding to each formatter. (The formatters are stored in the * order they occur in the pattern, not in the order in which the arguments are specified.) + * Is set in {@link #parse}. */ - private static List argumentIndices; + private static @MonotonicNonNull List argumentIndices; + // I think this means the number of format specifiers in the format string. /** The number of subformats. */ private static int numFormat; @@ -161,6 +178,7 @@ private static class MessageFormatParser { "", "short", "medium", "long", "full" }; + @EnsuresNonNull({"categories", "argumentIndices", "locale"}) public static I18nConversion[] parse(String pattern) { MessageFormatParser.categories = new ArrayList<>(); MessageFormatParser.argumentIndices = new ArrayList<>(); @@ -174,8 +192,10 @@ public static I18nConversion[] parse(String pattern) { return ret; } + @SuppressWarnings("nullness:dereference.of.nullable") // complex rules for segments[i] + @RequiresNonNull({"argumentIndices", "categories", "locale"}) private static void applyPattern(String pattern) { - StringBuilder[] segments = new StringBuilder[4]; + @Nullable StringBuilder[] segments = new StringBuilder[4]; // Allocate only segments[SEG_RAW] here. The rest are // allocated on demand. segments[SEG_RAW] = new StringBuilder(); @@ -262,7 +282,8 @@ private static void applyPattern(String pattern) { } /** Side-effects {@code categories} field, adding to it an I18nConversionCategory. */ - private static void makeFormat(int offsetNumber, StringBuilder[] textSegments) { + @RequiresNonNull({"argumentIndices", "categories", "locale"}) + private static void makeFormat(int offsetNumber, @Nullable StringBuilder[] textSegments) { String[] segments = new String[textSegments.length]; for (int i = 0; i < textSegments.length; i++) { StringBuilder oneseg = textSegments[i]; @@ -372,7 +393,8 @@ private static final int findKeyword(String s, String[] list) { } // Try trimmed lowercase. - String ls = s.trim().toLowerCase(Locale.ROOT); + @SuppressWarnings("interning:assignment.type.incompatible") // test if value changed + @InternedDistinct String ls = s.trim().toLowerCase(Locale.ROOT); if (ls != s) { // Don't loop if the string trim().toLowerCase returned the same object. for (int i = 0; i < list.length; ++i) { if (ls.equals(list[i])) { 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 38d156f455c..f11bbcdf073 100644 --- a/checker/src/main/java/org/checkerframework/checker/i18nformatter/I18nFormatterTreeUtil.java +++ b/checker/src/main/java/org/checkerframework/checker/i18nformatter/I18nFormatterTreeUtil.java @@ -236,14 +236,19 @@ public I18nFormatCall createFormatForCall( /** * Represents a format method invocation in the syntax tree. * - *

An I18nFormatCall instance can only be instantiated by createFormatForCall method + *

An I18nFormatCall instance can only be instantiated by the createFormatForCall method. */ public class I18nFormatCall { - private final ExpressionTree tree; + /** The AST node for the call. */ + private final MethodInvocationTree tree; + /** The format string argument. */ private ExpressionTree formatArg; + /** The type factory. */ private final AnnotatedTypeFactory atypeFactory; + /** The arguments to the format string. */ private List args; + /** Extra description for error messages. */ private String invalidMessage; private AnnotatedTypeMirror formatAnno; @@ -261,6 +266,15 @@ public I18nFormatCall( initialCheck(theargs, method, node, methodAnno); } + /** + * Returns the AST node for the call. + * + * @return the AST node for the call + */ + public MethodInvocationTree getTree() { + return tree; + } + @Override public String toString() { return this.tree.toString(); @@ -420,7 +434,7 @@ public InvocationType visitNull(NullType t, Class p) { } ExpressionTree loc; - loc = ((MethodInvocationTree) tree).getMethodSelect(); + loc = tree.getMethodSelect(); if (type != InvocationType.VARARG && !args.isEmpty()) { loc = args.get(0); } diff --git a/checker/src/main/java/org/checkerframework/checker/i18nformatter/I18nFormatterVisitor.java b/checker/src/main/java/org/checkerframework/checker/i18nformatter/I18nFormatterVisitor.java index 125cfb266b8..6ab4583691a 100644 --- a/checker/src/main/java/org/checkerframework/checker/i18nformatter/I18nFormatterVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/i18nformatter/I18nFormatterVisitor.java @@ -3,6 +3,8 @@ import com.sun.source.tree.MethodInvocationTree; import com.sun.source.tree.Tree; import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.Name; import javax.lang.model.type.TypeMirror; import org.checkerframework.checker.compilermsgs.qual.CompilerMessageKey; import org.checkerframework.checker.formatter.FormatterTreeUtil.InvocationType; @@ -16,6 +18,7 @@ import org.checkerframework.dataflow.cfg.node.MethodInvocationNode; import org.checkerframework.framework.type.AnnotatedTypeMirror; import org.checkerframework.javacutil.AnnotationUtils; +import org.checkerframework.javacutil.TreeUtils; /** * Whenever a method with {@link I18nFormatFor} annotation is invoked, it will perform the format @@ -47,8 +50,6 @@ private void checkInvocationFormatFor(I18nFormatCall fc) { I18nFormatterTreeUtil tu = atypeFactory.treeUtil; Result type = fc.getFormatType(); - Result invc; - I18nConversionCategory[] formatCats; switch (type.value()) { case I18NINVALID: tu.failure(type, "i18nformat.string.invalid", fc.getInvalidError()); @@ -60,8 +61,8 @@ private void checkInvocationFormatFor(I18nFormatCall fc) { } break; case I18NFORMAT: - invc = fc.getInvocationType(); - formatCats = fc.getFormatCategories(); + Result invc = fc.getInvocationType(); + I18nConversionCategory[] formatCats = fc.getFormatCategories(); switch (invc.value()) { case VARARG: Result[] paramTypes = fc.getParamTypes(); @@ -89,9 +90,14 @@ private void checkInvocationFormatFor(I18nFormatCall fc) { break; default: if (!fc.isValidParameter(formatCat, paramType)) { + ExecutableElement method = + TreeUtils.elementFromUse(fc.getTree()); + Name methodName = method.getSimpleName(); tu.failure( param, "argument.type.incompatible", + "", // parameter name is not useful + methodName, paramType, formatCat); } @@ -122,7 +128,8 @@ protected void commonAssignmentCheck( AnnotatedTypeMirror varType, AnnotatedTypeMirror valueType, Tree valueTree, - @CompilerMessageKey String errorKey) { + @CompilerMessageKey String errorKey, + Object... extraArgs) { AnnotationMirror rhs = valueType.getAnnotationInHierarchy(atypeFactory.I18NUNKNOWNFORMAT); AnnotationMirror lhs = varType.getAnnotationInHierarchy(atypeFactory.I18NUNKNOWNFORMAT); @@ -163,6 +170,6 @@ protected void commonAssignmentCheck( // issued for a given line of code will take precedence over the // assignment.type.incompatible // issued by super.commonAssignmentCheck. - super.commonAssignmentCheck(varType, valueType, valueTree, errorKey); + super.commonAssignmentCheck(varType, valueType, valueTree, errorKey, extraArgs); } } diff --git a/checker/src/main/java/org/checkerframework/checker/i18nformatter/qual/I18nConversionCategory.java b/checker/src/main/java/org/checkerframework/checker/i18nformatter/qual/I18nConversionCategory.java index d1d816917c9..6b697978c33 100644 --- a/checker/src/main/java/org/checkerframework/checker/i18nformatter/qual/I18nConversionCategory.java +++ b/checker/src/main/java/org/checkerframework/checker/i18nformatter/qual/I18nConversionCategory.java @@ -4,25 +4,23 @@ import java.util.Date; import java.util.HashSet; import java.util.Set; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.framework.qual.AnnotatedFor; /** * Elements of this enumeration are used in a {@link I18nFormat} annotation to indicate the valid * types that may be passed as a format parameter. For example: * - *

- * - *
{@literal @}I18nFormat({I18nConversionCategory.GENERAL, I18nConversionCategory.NUMBER})
- * String f = "{0}{1, number}";
+ * 
{@literal @}I18nFormat({GENERAL, NUMBER}) String f = "{0}{1, number}";
  * MessageFormat.format(f, "Example", 0) // valid
* - *
- * * The annotation indicates that the format string requires any object as the first parameter * ({@link I18nConversionCategory#GENERAL}) and a number as the second parameter ({@link * I18nConversionCategory#NUMBER}). * * @checker_framework.manual #i18n-formatter-checker Internationalization Format String Checker */ +@AnnotatedFor("nullness") public enum I18nConversionCategory { /** @@ -54,12 +52,12 @@ public enum I18nConversionCategory { NUMBER(new Class[] {Number.class}, new String[] {"number", "choice"}); @SuppressWarnings("ImmutableEnumChecker") // TODO: clean this up! - public final Class[] types; + public final Class @Nullable [] types; @SuppressWarnings("ImmutableEnumChecker") // TODO: clean this up! - public final String[] strings; + public final String @Nullable [] strings; - I18nConversionCategory(Class[] types, String[] strings) { + I18nConversionCategory(Class @Nullable [] types, String @Nullable [] strings) { this.types = types; this.strings = strings; } @@ -76,6 +74,8 @@ public enum I18nConversionCategory { * * @return the I18nConversionCategory associated with the given string */ + @SuppressWarnings( + "nullness:iterating.over.nullable") // in namedCategories, `strings` field is non-null public static I18nConversionCategory stringToI18nConversionCategory(String string) { string = string.toLowerCase(); for (I18nConversionCategory v : namedCategories) { @@ -127,11 +127,20 @@ public static I18nConversionCategory intersect( return a; } - Set> as = arrayToSet(a.types); - Set> bs = arrayToSet(b.types); + @SuppressWarnings( + "nullness:argument.type.incompatible") // types field is only null in UNUSED and + // GENERAL + Set> as = arrayToSet(a.types); + @SuppressWarnings( + "nullness:argument.type.incompatible") // types field is only null in UNUSED and + // GENERAL + Set> bs = arrayToSet(b.types); as.retainAll(bs); // intersection for (I18nConversionCategory v : new I18nConversionCategory[] {DATE, NUMBER}) { - Set> vs = arrayToSet(v.types); + @SuppressWarnings( + "nullness:argument.type.incompatible") // in those values, `types` field is + // non-null + Set> vs = arrayToSet(v.types); if (vs.equals(as)) { return v; } @@ -168,7 +177,7 @@ public String toString() { } else { sb.append(" conversion category (one of: "); boolean first = true; - for (Class cls : this.types) { + for (Class cls : this.types) { if (!first) { sb.append(", "); } diff --git a/checker/src/main/java/org/checkerframework/checker/index/inequality/LessThanVisitor.java b/checker/src/main/java/org/checkerframework/checker/index/inequality/LessThanVisitor.java index 0d608e56895..a6fd12c3283 100644 --- a/checker/src/main/java/org/checkerframework/checker/index/inequality/LessThanVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/index/inequality/LessThanVisitor.java @@ -24,7 +24,10 @@ public LessThanVisitor(BaseTypeChecker checker) { @Override protected void commonAssignmentCheck( - Tree varTree, ExpressionTree valueTree, @CompilerMessageKey String errorKey) { + Tree varTree, + ExpressionTree valueTree, + @CompilerMessageKey String errorKey, + Object... extraArgs) { // check that when an assignment to a variable declared as @HasSubsequence(a, from, to) // occurs, from <= to. @@ -53,7 +56,7 @@ protected void commonAssignmentCheck( } } - super.commonAssignmentCheck(varTree, valueTree, errorKey); + super.commonAssignmentCheck(varTree, valueTree, errorKey, extraArgs); } @Override @@ -61,7 +64,8 @@ protected void commonAssignmentCheck( AnnotatedTypeMirror varType, AnnotatedTypeMirror valueType, Tree valueTree, - @CompilerMessageKey String errorKey) { + @CompilerMessageKey String errorKey, + Object... extraArgs) { // If value is less than all expressions in the annotation in varType, // using the Value Checker, then skip the common assignment check. // Also skip the check if the only expression is "a + 1" and the valueTree @@ -98,7 +102,7 @@ protected void commonAssignmentCheck( return; } } - super.commonAssignmentCheck(varType, valueType, valueTree, errorKey); + super.commonAssignmentCheck(varType, valueType, valueTree, errorKey, extraArgs); } @Override diff --git a/checker/src/main/java/org/checkerframework/checker/index/lowerbound/LowerBoundVisitor.java b/checker/src/main/java/org/checkerframework/checker/index/lowerbound/LowerBoundVisitor.java index df3e84591d0..3cecd0ad5f6 100644 --- a/checker/src/main/java/org/checkerframework/checker/index/lowerbound/LowerBoundVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/index/lowerbound/LowerBoundVisitor.java @@ -62,7 +62,10 @@ public Void visitNewArray(NewArrayTree tree, Void type) { @Override protected void commonAssignmentCheck( - Tree varTree, ExpressionTree valueTree, @CompilerMessageKey String errorKey) { + Tree varTree, + ExpressionTree valueTree, + @CompilerMessageKey String errorKey, + Object... extraArgs) { // check that when an assignment to a variable declared as @HasSubsequence(a, from, to) // occurs, from is non-negative. @@ -88,6 +91,6 @@ protected void commonAssignmentCheck( } } - super.commonAssignmentCheck(varTree, valueTree, errorKey); + super.commonAssignmentCheck(varTree, valueTree, errorKey, extraArgs); } } diff --git a/checker/src/main/java/org/checkerframework/checker/index/samelen/SameLenVisitor.java b/checker/src/main/java/org/checkerframework/checker/index/samelen/SameLenVisitor.java index 613c17fd584..fda48689c13 100644 --- a/checker/src/main/java/org/checkerframework/checker/index/samelen/SameLenVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/index/samelen/SameLenVisitor.java @@ -33,7 +33,8 @@ protected void commonAssignmentCheck( AnnotatedTypeMirror varType, AnnotatedTypeMirror valueType, Tree valueTree, - @CompilerMessageKey String errorKey) { + @CompilerMessageKey String errorKey, + Object... extraArgs) { if (IndexUtil.isSequenceType(valueType.getUnderlyingType()) && TreeUtils.isExpressionTree(valueTree) // if both annotations are @PolySameLen, there is nothing to do @@ -57,6 +58,6 @@ protected void commonAssignmentCheck( valueType.replaceAnnotation(newSameLen); } } - super.commonAssignmentCheck(varType, valueType, valueTree, errorKey); + super.commonAssignmentCheck(varType, valueType, valueTree, errorKey, extraArgs); } } diff --git a/checker/src/main/java/org/checkerframework/checker/index/upperbound/UpperBoundVisitor.java b/checker/src/main/java/org/checkerframework/checker/index/upperbound/UpperBoundVisitor.java index 35b0d8e8126..95141c62b43 100644 --- a/checker/src/main/java/org/checkerframework/checker/index/upperbound/UpperBoundVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/index/upperbound/UpperBoundVisitor.java @@ -190,7 +190,10 @@ private void visitAccess(ExpressionTree indexTree, ExpressionTree arrTree) { @Override protected void commonAssignmentCheck( - Tree varTree, ExpressionTree valueTree, @CompilerMessageKey String errorKey) { + Tree varTree, + ExpressionTree valueTree, + @CompilerMessageKey String errorKey, + Object... extraArgs) { // check that when an assignment to a variable b declared as @HasSubsequence(a, from, to) // occurs, to <= a.length, i.e. to is @LTEqLengthOf(a). @@ -237,14 +240,15 @@ protected void commonAssignmentCheck( } } - super.commonAssignmentCheck(varTree, valueTree, errorKey); + super.commonAssignmentCheck(varTree, valueTree, errorKey, extraArgs); } @Override protected void commonAssignmentCheck( AnnotatedTypeMirror varType, ExpressionTree valueTree, - @CompilerMessageKey String errorKey) { + @CompilerMessageKey String errorKey, + Object... extraArgs) { AnnotatedTypeMirror valueType = atypeFactory.getAnnotatedType(valueTree); commonAssignmentCheckStartDiagnostic(varType, valueType, valueTree); if (!relaxedCommonAssignment(varType, valueTree)) { @@ -253,7 +257,7 @@ protected void commonAssignmentCheck( varType, valueType, valueTree); - super.commonAssignmentCheck(varType, valueTree, errorKey); + super.commonAssignmentCheck(varType, valueTree, errorKey, extraArgs); } else if (checker.hasOption("showchecks")) { commonAssignmentCheckEndDiagnostic( true, "relaxedCommonAssignment", varType, valueType, valueTree); diff --git a/checker/src/main/java/org/checkerframework/checker/initialization/InitializationAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/initialization/InitializationAnnotatedTypeFactory.java index 76fcda52282..964d3320d68 100644 --- a/checker/src/main/java/org/checkerframework/checker/initialization/InitializationAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/initialization/InitializationAnnotatedTypeFactory.java @@ -3,6 +3,7 @@ import com.sun.source.tree.ClassTree; import com.sun.source.tree.ExpressionTree; import com.sun.source.tree.LiteralTree; +import com.sun.source.tree.MemberSelectTree; import com.sun.source.tree.MethodTree; import com.sun.source.tree.NewClassTree; import com.sun.source.tree.Tree; @@ -427,12 +428,17 @@ public AnnotatedDeclaredType getSelfType(Tree tree) { } /** - * In the first enclosing class, find the top-level member that contains tree. TODO: should we - * look whether these elements are enclosed within another class that is itself under - * construction. + * In the first enclosing class, find the top-level member that contains {@code path}. + * + *

TODO: should we look whether these elements are enclosed within another class that is + * itself under construction. * *

Are there any other type of top level objects? + * + * @param path the path whose leaf is the target + * @return a top-level member containing the leaf of {@code path} */ + @SuppressWarnings("interning:not.interned") // AST node comparison private Tree findTopLevelClassMemberForTree(TreePath path) { ClassTree enclosingClass = TreeUtils.enclosingClass(path); if (enclosingClass != null) { @@ -737,6 +743,15 @@ public Void visitLiteral(LiteralTree tree, AnnotatedTypeMirror type) { } return super.visitLiteral(tree, type); } + + @Override + public Void visitMemberSelect( + MemberSelectTree node, AnnotatedTypeMirror annotatedTypeMirror) { + if (TreeUtils.isArrayLengthAccess(node)) { + annotatedTypeMirror.replaceAnnotation(INITIALIZED); + } + return super.visitMemberSelect(node, annotatedTypeMirror); + } } /** diff --git a/checker/src/main/java/org/checkerframework/checker/initialization/InitializationTransfer.java b/checker/src/main/java/org/checkerframework/checker/initialization/InitializationTransfer.java index b539d07bb12..d60ac81e296 100644 --- a/checker/src/main/java/org/checkerframework/checker/initialization/InitializationTransfer.java +++ b/checker/src/main/java/org/checkerframework/checker/initialization/InitializationTransfer.java @@ -13,7 +13,6 @@ import javax.lang.model.type.TypeKind; import javax.lang.model.type.TypeMirror; import javax.lang.model.util.ElementFilter; -import org.checkerframework.dataflow.analysis.ConditionalTransferResult; import org.checkerframework.dataflow.analysis.FlowExpressions; import org.checkerframework.dataflow.analysis.FlowExpressions.FieldAccess; import org.checkerframework.dataflow.analysis.FlowExpressions.Receiver; @@ -86,9 +85,11 @@ protected boolean isNotFullyInitializedReceiver(MethodTree methodTree) { /** * Returns the fields that can safely be considered initialized after the method call {@code * node}. + * + * @param node a method call + * @return the fields that are initialized after the method call */ - protected List initializedFieldsAfterCall( - MethodInvocationNode node, ConditionalTransferResult transferResult) { + protected List initializedFieldsAfterCall(MethodInvocationNode node) { List result = new ArrayList<>(); MethodInvocationTree tree = node.getTree(); ExecutableElement method = TreeUtils.elementFromUse(tree); @@ -194,9 +195,7 @@ public TransferResult visitFieldAccess(FieldAccessNode n, TransferInput visitMethodInvocation( MethodInvocationNode n, TransferInput in) { TransferResult result = super.visitMethodInvocation(n, in); - assert result instanceof ConditionalTransferResult; - List newlyInitializedFields = - initializedFieldsAfterCall(n, (ConditionalTransferResult) result); + List newlyInitializedFields = initializedFieldsAfterCall(n); if (!newlyInitializedFields.isEmpty()) { for (VariableElement f : newlyInitializedFields) { result.getThenStore().addInitializedField(f); 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 44c370f48fd..fcc5e084794 100644 --- a/checker/src/main/java/org/checkerframework/checker/initialization/InitializationVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/initialization/InitializationVisitor.java @@ -110,7 +110,10 @@ protected void checkThisOrSuperConstructorCall( @Override protected void commonAssignmentCheck( - Tree varTree, ExpressionTree valueExp, @CompilerMessageKey String errorKey) { + Tree varTree, + ExpressionTree valueExp, + @CompilerMessageKey String errorKey, + Object... extraArgs) { // field write of the form x.f = y if (TreeUtils.isFieldAccess(varTree)) { // cast is safe: a field access can only be an IdentifierTree or @@ -141,7 +144,7 @@ protected void commonAssignmentCheck( } } } - super.commonAssignmentCheck(varTree, valueExp, errorKey); + super.commonAssignmentCheck(varTree, valueExp, errorKey, extraArgs); } @Override diff --git a/checker/src/main/java/org/checkerframework/checker/interning/InterningAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/interning/InterningAnnotatedTypeFactory.java index ffd7f193972..cb2ba0e93be 100644 --- a/checker/src/main/java/org/checkerframework/checker/interning/InterningAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/interning/InterningAnnotatedTypeFactory.java @@ -2,6 +2,7 @@ import com.sun.source.tree.BinaryTree; import com.sun.source.tree.CompoundAssignmentTree; +import com.sun.source.tree.IdentifierTree; import com.sun.source.tree.Tree; import com.sun.source.tree.TypeCastTree; import com.sun.tools.javac.code.Symbol.MethodSymbol; @@ -12,8 +13,10 @@ import javax.lang.model.type.DeclaredType; import javax.lang.model.type.TypeKind; import javax.lang.model.type.TypeMirror; +import org.checkerframework.checker.interning.qual.FindDistinct; import org.checkerframework.checker.interning.qual.InternMethod; import org.checkerframework.checker.interning.qual.Interned; +import org.checkerframework.checker.interning.qual.InternedDistinct; import org.checkerframework.checker.interning.qual.PolyInterned; import org.checkerframework.checker.interning.qual.UnknownInterned; import org.checkerframework.common.basetype.BaseAnnotatedTypeFactory; @@ -47,11 +50,14 @@ *

  • is a use of a class declared to be @Interned * * - * This factory extends {@link BaseAnnotatedTypeFactory} and inherits its functionality, including: - * flow-sensitive qualifier inference, qualifier polymorphism (of {@link PolyInterned}), implicit - * annotations via {@link org.checkerframework.framework.qual.DefaultFor} on {@link Interned} (to - * handle cases 1, 2, 4), and user-specified defaults via {@link DefaultQualifier}. Case 5 is - * handled by the stub library. + * This type factory adds {@link InternedDistinct} to formal parameters that have a {@link + * FindDistinct} declaration annotation. + * + *

    This factory extends {@link BaseAnnotatedTypeFactory} and inherits its functionality, + * including: flow-sensitive qualifier inference, qualifier polymorphism (of {@link PolyInterned}), + * implicit annotations via {@link org.checkerframework.framework.qual.DefaultFor} on {@link + * Interned} (to handle cases 1, 2, 4), and user-specified defaults via {@link DefaultQualifier}. + * Case 5 is handled by the stub library. */ public class InterningAnnotatedTypeFactory extends BaseAnnotatedTypeFactory { @@ -59,6 +65,9 @@ public class InterningAnnotatedTypeFactory extends BaseAnnotatedTypeFactory { final AnnotationMirror TOP = AnnotationBuilder.fromClass(elements, UnknownInterned.class); /** The {@link Interned} annotation. */ final AnnotationMirror INTERNED = AnnotationBuilder.fromClass(elements, Interned.class); + /** The {@link InternedDistinct} annotation. */ + final AnnotationMirror INTERNED_DISTINCT = + AnnotationBuilder.fromClass(elements, InternedDistinct.class); /** * Creates a new {@link InterningAnnotatedTypeFactory} that operates on a particular AST. @@ -185,6 +194,15 @@ public Void visitTypeCast(TypeCastTree node, AnnotatedTypeMirror type) { } return super.visitTypeCast(node, type); } + + @Override + public Void visitIdentifier(IdentifierTree node, AnnotatedTypeMirror type) { + Element e = TreeUtils.elementFromTree(node); + if (atypeFactory.getDeclAnnotation(e, FindDistinct.class) != null) { + type.replaceAnnotation(INTERNED_DISTINCT); + } + return super.visitIdentifier(node, type); + } } /** Adds @Interned to enum types. */ 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 eb6285e2001..c939ed7e5b0 100644 --- a/checker/src/main/java/org/checkerframework/checker/interning/InterningVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/interning/InterningVisitor.java @@ -24,12 +24,15 @@ import javax.lang.model.element.Element; 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.type.DeclaredType; import javax.lang.model.type.TypeKind; import javax.lang.model.type.TypeMirror; import javax.lang.model.util.ElementFilter; import javax.tools.Diagnostic.Kind; +import org.checkerframework.checker.interning.qual.CompareToMethod; +import org.checkerframework.checker.interning.qual.EqualsMethod; import org.checkerframework.checker.interning.qual.InternMethod; import org.checkerframework.checker.interning.qual.Interned; import org.checkerframework.checker.interning.qual.InternedDistinct; @@ -175,25 +178,20 @@ public Void visitBinary(BinaryTree node, Void p) { return super.visitBinary(node, p); } - Element leftElt = null; - Element rightElt = null; - if (left instanceof AnnotatedTypeMirror.AnnotatedDeclaredType) { - leftElt = ((DeclaredType) left.getUnderlyingType()).asElement(); - } - if (right instanceof AnnotatedTypeMirror.AnnotatedDeclaredType) { - rightElt = ((DeclaredType) right.getUnderlyingType()).asElement(); - } - - // TODO: CODE REVIEW - // TODO: WOULD IT BE CLEARER TO USE A METHOD usesReferenceEquality(AnnotatedTypeMirror type) - // TODO: RATHER THAN leftElt.getAnnotation(UsesObjectEquals.class) != null) - // if neither @Interned or @UsesObjectEquals, report error + Element leftElt = TypesUtils.getTypeElement(left.getUnderlyingType()); + // If neither @Interned or @UsesObjectEquals, report error. if (!(left.hasEffectiveAnnotation(INTERNED) - || (leftElt != null && leftElt.getAnnotation(UsesObjectEquals.class) != null))) { + || (leftElt != null + && atypeFactory.getDeclAnnotation(leftElt, UsesObjectEquals.class) + != null))) { checker.reportError(leftOp, "not.interned", left); } + + Element rightElt = TypesUtils.getTypeElement(right.getUnderlyingType()); if (!(right.hasEffectiveAnnotation(INTERNED) - || (rightElt != null && rightElt.getAnnotation(UsesObjectEquals.class) != null))) { + || (rightElt != null + && atypeFactory.getDeclAnnotation(rightElt, UsesObjectEquals.class) + != null))) { checker.reportError(rightOp, "not.interned", right); } return super.visitBinary(node, p); @@ -219,13 +217,44 @@ public Void visitMethodInvocation(MethodInvocationTree node, Void p) { return super.visitMethodInvocation(node, p); } + // Ensure that method annotations are not written on methods they don't apply to. + @Override + public Void visitMethod(MethodTree node, Void p) { + ExecutableElement methElt = TreeUtils.elementFromDeclaration(node); + boolean hasCompareToMethodAnno = + atypeFactory.getDeclAnnotation(methElt, CompareToMethod.class) != null; + boolean hasEqualsMethodAnno = + atypeFactory.getDeclAnnotation(methElt, EqualsMethod.class) != null; + boolean hasInternMethodAnno = + atypeFactory.getDeclAnnotation(methElt, InternMethod.class) != null; + int params = methElt.getParameters().size(); + if (hasCompareToMethodAnno && !(params == 1 || params == 2)) { + checker.reportError( + node, + "invalid.method.annotation", + "@CompareToMethod", + "1 or 2", + methElt, + params); + } else if (hasEqualsMethodAnno && !(params == 1 || params == 2)) { + checker.reportError( + node, "invalid.method.annotation", "@EqualsMethod", "1 or 2", methElt, params); + } else if (hasInternMethodAnno && !(params == 0)) { + checker.reportError( + node, "invalid.method.annotation", "@InternMethod", "0", methElt, params); + } + + return super.visitMethod(node, p); + } + /** * Method to implement the @UsesObjectEquals functionality. If a class is annotated * with @UsesObjectEquals, it must: * *

      - *
    • not override .equals(Object) - *
    • be a subclass of Object or another class annotated with @UsesObjectEquals + *
    • not override .equals(Object) and be a subclass of a class annotated + * with @UsesObjectEquals, or + *
    • override equals(Object) with body "this == arg" *
    * * If a class is not annotated with @UsesObjectEquals, it must: @@ -246,20 +275,25 @@ public void processClassTree(ClassTree classTree) { // If @UsesObjectEquals is present, check to make sure the class does not override equals // and its supertype is Object or is annotated with @UsesObjectEquals. if (annotation != null) { - // Check methods to ensure no .equals - if (overridesEquals(classTree)) { - checker.reportError(classTree, "overrides.equals"); - } - TypeMirror superClass = elt.getSuperclass(); - if (superClass != null - // The super class of an interface is "none" rather than null. - && superClass.getKind() == TypeKind.DECLARED) { - TypeElement superClassElement = TypesUtils.getTypeElement(superClass); - if (superClassElement != null - && !ElementUtils.isObject(superClassElement) - && atypeFactory.getDeclAnnotation(superClassElement, UsesObjectEquals.class) - == null) { - checker.reportError(classTree, "superclass.notannotated"); + MethodTree equalsMethod = equalsImplementation(classTree); + if (equalsMethod != null) { + if (!isReferenceEqualityImplementation(equalsMethod)) { + checker.reportError(classTree, "overrides.equals"); + } + } else { + // Does not override equals() + TypeMirror superClass = elt.getSuperclass(); + if (superClass != null + // The super class of an interface is "none" rather than null. + && superClass.getKind() == TypeKind.DECLARED) { + TypeElement superClassElement = TypesUtils.getTypeElement(superClass); + if (superClassElement != null + && !ElementUtils.isObject(superClassElement) + && atypeFactory.getDeclAnnotation( + superClassElement, UsesObjectEquals.class) + == null) { + checker.reportError(classTree, "superclass.notannotated"); + } } } } @@ -267,6 +301,41 @@ public void processClassTree(ClassTree classTree) { super.processClassTree(classTree); } + /** + * Returns true if the given equals() method implements reference equality. + * + * @param equalsMethod an overriding implementation of Object.equals() + * @return true if the given equals() method implements reference equality + */ + private boolean isReferenceEqualityImplementation(MethodTree equalsMethod) { + BlockTree body = equalsMethod.getBody(); + List bodyStatements = body.getStatements(); + if (bodyStatements.size() == 1) { + StatementTree bodyStatement = bodyStatements.get(0); + if (bodyStatement.getKind() == Tree.Kind.RETURN) { + ExpressionTree returnExpr = + TreeUtils.withoutParens(((ReturnTree) bodyStatement).getExpression()); + if (returnExpr.getKind() == Tree.Kind.EQUAL_TO) { + BinaryTree bt = (BinaryTree) returnExpr; + ExpressionTree lhsTree = bt.getLeftOperand(); + ExpressionTree rhsTree = bt.getRightOperand(); + if (lhsTree.getKind() == Tree.Kind.IDENTIFIER + && rhsTree.getKind() == Tree.Kind.IDENTIFIER) { + Name leftName = ((IdentifierTree) lhsTree).getName(); + Name rightName = ((IdentifierTree) rhsTree).getName(); + Name paramName = equalsMethod.getParameters().get(0).getName(); + if ((leftName.contentEquals("this") && rightName.equals(paramName)) + || (leftName.equals(paramName) + && rightName.contentEquals("this"))) { + return true; + } + } + } + } + } + return false; + } + @Override protected void checkConstructorResult( AnnotatedExecutableType constructorType, ExecutableElement constructorElement) { @@ -340,19 +409,24 @@ private boolean checkCreationOfInternedObject( // Helper methods // ********************************************************************** - /** Returns true if a class overrides Object.equals. */ - private boolean overridesEquals(ClassTree node) { + /** + * Returns the method that overrides Object.equals, or null. + * + * @param node a class + * @return the class's implementation of equals, or null + */ + private MethodTree equalsImplementation(ClassTree node) { List members = node.getMembers(); for (Tree member : members) { if (member instanceof MethodTree) { MethodTree mTree = (MethodTree) member; ExecutableElement enclosing = TreeUtils.elementFromDeclaration(mTree); if (overrides(enclosing, Object.class, "equals")) { - return true; + return mTree; } } } - return false; + return null; } /** @@ -408,42 +482,51 @@ private boolean suppressInsideComparison(final BinaryTree node) { return false; } - // If we're not directly in an if statement in a method (ignoring - // parens and blocks), terminate. - if (!Heuristics.matchParents(getCurrentPath(), Tree.Kind.IF, Tree.Kind.METHOD)) { - return false; - } - - // Ensure the if statement is the first statement in the method - - TreePath parentPath = getCurrentPath().getParentPath(); - - // Retrieve the enclosing if statement tree and method tree - Tree tree, ifStatementTree = null; - MethodTree methodTree = null; - while ((tree = parentPath.getLeaf()) != null) { - if (tree.getKind() == Tree.Kind.IF) { - ifStatementTree = tree; - } else if (tree.getKind() == Tree.Kind.METHOD) { - methodTree = (MethodTree) tree; - break; + TreePath path = getCurrentPath(); + TreePath parentPath = path.getParentPath(); + Tree parent = parentPath.getLeaf(); + + // Ensure the == is in a return or in an if, and that enclosing statement is the first + // statement in the method. + if (parent.getKind() == Tree.Kind.RETURN) { + // ensure the return statement is the first statement in the method + if (parentPath.getParentPath().getParentPath().getLeaf().getKind() + != Tree.Kind.METHOD) { + return false; } - parentPath = parentPath.getParentPath(); - } - - // The call to Heuristics.matchParents already ensured there is an enclosing if statement - assert ifStatementTree != null; - // The call to Heuristics.matchParents already ensured there is an enclosing method - assert methodTree != null; - - StatementTree stmnt = methodTree.getBody().getStatements().get(0); - // The call to Heuristics.matchParents already ensured the enclosing method has at least one - // statement (an if statement) in the body - assert stmnt != null; - - if (stmnt != ifStatementTree) { - return false; // The if statement is not the first statement in the method. + // maybe set some variables?? + } else if (Heuristics.matchParents(getCurrentPath(), Tree.Kind.IF, Tree.Kind.METHOD)) { + // Ensure the if statement is the first statement in the method + + // Retrieve the enclosing if statement tree and method tree + Tree ifStatementTree = null; + MethodTree methodTree = null; + // Set ifStatementTree and methodTree + { + TreePath ppath = parentPath; + Tree tree; + while ((tree = ppath.getLeaf()) != null) { + if (tree.getKind() == Tree.Kind.IF) { + ifStatementTree = tree; + } else if (tree.getKind() == Tree.Kind.METHOD) { + methodTree = (MethodTree) tree; + break; + } + ppath = ppath.getParentPath(); + } + } + assert ifStatementTree != null; + assert methodTree != null; + StatementTree firstStmnt = methodTree.getBody().getStatements().get(0); + assert firstStmnt != null; + @SuppressWarnings("interning:not.interned") // comparing AST nodes + boolean notSameNode = firstStmnt != ifStatementTree; + if (notSameNode) { + return false; // The if statement is not the first statement in the method. + } + } else { + return false; } ExecutableElement enclosingMethod = @@ -479,9 +562,16 @@ public Boolean visitReturn(ReturnTree tree, Void p) { } }; + boolean hasCompareToMethodAnno = + atypeFactory.getDeclAnnotation(enclosingMethod, CompareToMethod.class) != null; + boolean hasEqualsMethodAnno = + atypeFactory.getDeclAnnotation(enclosingMethod, EqualsMethod.class) != null; + int params = enclosingMethod.getParameters().size(); + // Determine whether or not the "then" statement of the if has a single // "return 0" statement (for the Comparator.compare heuristic). - if (overrides(enclosingMethod, Comparator.class, "compare")) { + if (overrides(enclosingMethod, Comparator.class, "compare") + || (hasCompareToMethodAnno && params == 2)) { final boolean returnsZero = new Heuristics.Within(new Heuristics.OfKind(Tree.Kind.IF, matcherIfReturnsZero)) .match(getCurrentPath()); @@ -490,20 +580,27 @@ public Boolean visitReturn(ReturnTree tree, Void p) { return false; } - assert enclosingMethod.getParameters().size() == 2; + assert params == 2; Element p1 = enclosingMethod.getParameters().get(0); Element p2 = enclosingMethod.getParameters().get(1); return (p1.equals(lhs) && p2.equals(rhs)) || (p1.equals(rhs) && p2.equals(lhs)); - } else if (overrides(enclosingMethod, Object.class, "equals")) { - assert enclosingMethod.getParameters().size() == 1; + } else if (overrides(enclosingMethod, Object.class, "equals") + || (hasEqualsMethodAnno && params == 1)) { + assert params == 1; Element param = enclosingMethod.getParameters().get(0); Element thisElt = getThis(trees.getScope(getCurrentPath())); assert thisElt != null; return (thisElt.equals(lhs) && param.equals(rhs)) || (thisElt.equals(rhs) && param.equals(lhs)); - } else if (overrides(enclosingMethod, Comparable.class, "compareTo")) { + } else if (hasEqualsMethodAnno && params == 2) { + Element p1 = enclosingMethod.getParameters().get(0); + Element p2 = enclosingMethod.getParameters().get(1); + return (p1.equals(lhs) && p2.equals(rhs)) || (p1.equals(rhs) && p2.equals(lhs)); + + } else if (overrides(enclosingMethod, Comparable.class, "compareTo") + || (hasCompareToMethodAnno && params == 1)) { final boolean returnsZero = new Heuristics.Within(new Heuristics.OfKind(Tree.Kind.IF, matcherIfReturnsZero)) @@ -513,13 +610,14 @@ public Boolean visitReturn(ReturnTree tree, Void p) { return false; } - assert enclosingMethod.getParameters().size() == 1; + assert params == 1; Element param = enclosingMethod.getParameters().get(0); Element thisElt = getThis(trees.getScope(getCurrentPath())); assert thisElt != null; return (thisElt.equals(lhs) && param.equals(rhs)) || (thisElt.equals(rhs) && param.equals(lhs)); } + return false; } @@ -723,7 +821,10 @@ public Boolean visitBinary(BinaryTree tree, Void p) { return visit(leftTree, p); } else { // a == b || a.compareTo(b) == 0 - ExpressionTree leftTree = tree.getLeftOperand(); // looking for a==b + @SuppressWarnings( + "interning:assignment.type.incompatible" // AST node comparisons + ) + @InternedDistinct ExpressionTree leftTree = tree.getLeftOperand(); // looking for a==b ExpressionTree rightTree = tree.getRightOperand(); // looking for a.compareTo(b) == 0 // or b.compareTo(a) == 0 diff --git a/checker/src/main/java/org/checkerframework/checker/interning/messages.properties b/checker/src/main/java/org/checkerframework/checker/interning/messages.properties index 72399842895..435cb6541ea 100644 --- a/checker/src/main/java/org/checkerframework/checker/interning/messages.properties +++ b/checker/src/main/java/org/checkerframework/checker/interning/messages.properties @@ -5,3 +5,4 @@ overrides.equals=annotated with @UsesObjectEquals but overrides .equals(Object) superclass.notannotated=superclass must be annotated with @UsesObjectEquals superclass.annotated=subclasses must also be annotated with @UsesObjectEquals interned.object.creation=Cannot statically verify that a new object of an @Interned class is @Interned +invalid.method.annotation=%s applies to a method with %s formal parameters; %s has %d diff --git a/checker/src/main/java/org/checkerframework/checker/interning/qual/CompareToMethod.java b/checker/src/main/java/org/checkerframework/checker/interning/qual/CompareToMethod.java new file mode 100644 index 00000000000..8fc5212de46 --- /dev/null +++ b/checker/src/main/java/org/checkerframework/checker/interning/qual/CompareToMethod.java @@ -0,0 +1,21 @@ +package org.checkerframework.checker.interning.qual; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import org.checkerframework.framework.qual.InheritedAnnotation; + +/** + * Method declaration annotation that indicates a method has a specification like {@code + * compareTo()} or {@code compare()}. The Interning Checker permits use of {@code if (this == arg) { + * return 0; }} or {@code if (arg1 == arg2) { return 0; }} within the body. + * + * @checker_framework.manual #interning-checker Interning Checker + */ +@Documented +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@InheritedAnnotation +public @interface CompareToMethod {} diff --git a/checker/src/main/java/org/checkerframework/checker/interning/qual/EqualsMethod.java b/checker/src/main/java/org/checkerframework/checker/interning/qual/EqualsMethod.java new file mode 100644 index 00000000000..727024e2759 --- /dev/null +++ b/checker/src/main/java/org/checkerframework/checker/interning/qual/EqualsMethod.java @@ -0,0 +1,21 @@ +package org.checkerframework.checker.interning.qual; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import org.checkerframework.framework.qual.InheritedAnnotation; + +/** + * Method declaration annotation that indicates a method has a specification like {@code equals()}. + * The Interning Checker permits use of {@code this == arg} within the body. Can also be applied to + * a static two-argument method, in which case {@code arg1 == arg2} is permitted within the body. + * + * @checker_framework.manual #interning-checker Interning Checker + */ +@Documented +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@InheritedAnnotation +public @interface EqualsMethod {} diff --git a/checker/src/main/java/org/checkerframework/checker/interning/qual/FindDistinct.java b/checker/src/main/java/org/checkerframework/checker/interning/qual/FindDistinct.java new file mode 100644 index 00000000000..9797d7c2363 --- /dev/null +++ b/checker/src/main/java/org/checkerframework/checker/interning/qual/FindDistinct.java @@ -0,0 +1,24 @@ +package org.checkerframework.checker.interning.qual; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * This formal parameter annotation indicates that the method searches for the given value, using + * reference equality ({@code ==}). + * + *

    Within the method, the formal parameter is treated as {@code @}{@link InternedDistinct}: it + * should be compared with {@code ==} rather than with {@code equals()}. However, any value may be + * passed to the method, and the Interning Checker does not verify that use of {@code ==} within the + * method is logically correct. + * + * @see org.checkerframework.checker.interning.InterningChecker + * @checker_framework.manual #interning-checker Interning Checker + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.PARAMETER}) +public @interface FindDistinct {} diff --git a/checker/src/main/java/org/checkerframework/checker/interning/qual/InternMethod.java b/checker/src/main/java/org/checkerframework/checker/interning/qual/InternMethod.java index 7385ba20cf0..9157a728860 100644 --- a/checker/src/main/java/org/checkerframework/checker/interning/qual/InternMethod.java +++ b/checker/src/main/java/org/checkerframework/checker/interning/qual/InternMethod.java @@ -2,10 +2,10 @@ import java.lang.annotation.Documented; import java.lang.annotation.ElementType; -import java.lang.annotation.Inherited; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import org.checkerframework.framework.qual.InheritedAnnotation; /** * Method declaration annotation used to indicate that this method may be invoked on an uninterned @@ -16,5 +16,5 @@ @Documented @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) -@Inherited +@InheritedAnnotation public @interface InternMethod {} 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 40f4e27dbdb..f109f81e135 100644 --- a/checker/src/main/java/org/checkerframework/checker/lock/LockVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/lock/LockVisitor.java @@ -343,7 +343,8 @@ protected void commonAssignmentCheck( AnnotatedTypeMirror varType, AnnotatedTypeMirror valueType, Tree valueTree, - @CompilerMessageKey String errorKey) { + @CompilerMessageKey String errorKey, + Object... extraArgs) { Kind valueTreeKind = valueTree.getKind(); @@ -430,7 +431,7 @@ protected void commonAssignmentCheck( } } - super.commonAssignmentCheck(varType, valueType, valueTree, errorKey); + super.commonAssignmentCheck(varType, valueType, valueTree, errorKey, extraArgs); } @Override @@ -1087,6 +1088,7 @@ private boolean isTreeSymbolEffectivelyFinalOrUnmodifiable(Tree tree) { } @Override + @SuppressWarnings("interning:not.interned") // AST node comparison public Void visitIdentifier(IdentifierTree tree, Void p) { // If the identifier is a field accessed via an implicit this, // then check the lock of this. (All other field accessed are checked in visitMemberSelect. diff --git a/checker/src/main/java/org/checkerframework/checker/lock/qual/GuardedBy.java b/checker/src/main/java/org/checkerframework/checker/lock/qual/GuardedBy.java index 31a8fc3330c..5e7b74902c0 100644 --- a/checker/src/main/java/org/checkerframework/checker/lock/qual/GuardedBy.java +++ b/checker/src/main/java/org/checkerframework/checker/lock/qual/GuardedBy.java @@ -11,6 +11,7 @@ import org.checkerframework.framework.qual.SubtypeOf; import org.checkerframework.framework.qual.TypeKind; import org.checkerframework.framework.qual.TypeUseLocation; +import org.checkerframework.framework.qual.UpperBoundFor; /** * Indicates that a thread may dereference the value referred to by the annotated variable only if @@ -52,7 +53,19 @@ TypeKind.LONG, TypeKind.SHORT }, - types = {java.lang.String.class, Void.class}) + types = {String.class, Void.class}) +@UpperBoundFor( + typeKinds = { + TypeKind.BOOLEAN, + TypeKind.BYTE, + TypeKind.CHAR, + TypeKind.DOUBLE, + TypeKind.FLOAT, + TypeKind.INT, + TypeKind.LONG, + TypeKind.SHORT + }, + types = String.class) public @interface GuardedBy { /** * The Java value expressions that need to be held. 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 b1f91db5088..e4e056e64eb 100644 --- a/checker/src/main/java/org/checkerframework/checker/nullness/NullnessAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/nullness/NullnessAnnotatedTypeFactory.java @@ -45,6 +45,8 @@ import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedArrayType; import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedDeclaredType; 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.QualifierHierarchy; import org.checkerframework.framework.type.treeannotator.ListTreeAnnotator; @@ -356,14 +358,30 @@ public AnnotatedTypeMirror getMethodReturnType(MethodTree m, ReturnTree r) { } @Override - protected TypeAnnotator createTypeAnnotator() { + protected DefaultForTypeAnnotator createDefaultForTypeAnnotator() { DefaultForTypeAnnotator defaultForTypeAnnotator = new DefaultForTypeAnnotator(this); - defaultForTypeAnnotator.addAtmClass(AnnotatedTypeMirror.AnnotatedNoType.class, NONNULL); - defaultForTypeAnnotator.addAtmClass( - AnnotatedTypeMirror.AnnotatedPrimitiveType.class, NONNULL); + defaultForTypeAnnotator.addAtmClass(AnnotatedNoType.class, NONNULL); + defaultForTypeAnnotator.addAtmClass(AnnotatedPrimitiveType.class, NONNULL); + return defaultForTypeAnnotator; + } + + @Override + protected void addAnnotationsFromDefaultForType( + @Nullable Element element, AnnotatedTypeMirror type) { + if (element != null + && element.getKind() == ElementKind.LOCAL_VARIABLE + && type.getKind().isPrimitive()) { + // Always apply the DefaultQualifierForUse for primitives. + super.addAnnotationsFromDefaultForType(null, type); + } else { + super.addAnnotationsFromDefaultForType(element, type); + } + } + + @Override + protected TypeAnnotator createTypeAnnotator() { return new ListTypeAnnotator( new PropagationTypeAnnotator(this), - defaultForTypeAnnotator, new NullnessTypeAnnotator(this), new CommitmentTypeAnnotator(this)); } diff --git a/checker/src/main/java/org/checkerframework/checker/nullness/NullnessUtil.java b/checker/src/main/java/org/checkerframework/checker/nullness/NullnessUtil.java index 1eee7405a79..127a55e80bc 100644 --- a/checker/src/main/java/org/checkerframework/checker/nullness/NullnessUtil.java +++ b/checker/src/main/java/org/checkerframework/checker/nullness/NullnessUtil.java @@ -3,6 +3,7 @@ import org.checkerframework.checker.nullness.qual.EnsuresNonNull; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.framework.qual.AnnotatedFor; /** * Utility class for the Nullness Checker. @@ -15,17 +16,15 @@ * *

    import static org.checkerframework.checker.nullness.NullnessUtil.*;
    * - *

    Runtime Dependency - * - *

    Please note that using this class introduces a runtime dependency. This means that you need to - * distribute (or link to) the Checker Framework, along with your binaries. - * - *

    To eliminate this dependency, you can simply copy this class into your own project. + *

    Runtime Dependency: If you use this class, you must distribute (or link to) {@code + * checker-qual.jar}, along with your binaries. Or, you can can copy this class into your own + * project. */ @SuppressWarnings({ "nullness", // Nullness utilities are trusted regarding nullness. "cast" // Casts look redundant if Nullness Checker is not run. }) +@AnnotatedFor("nullness") public final class NullnessUtil { private NullnessUtil() { @@ -63,7 +62,8 @@ private NullnessUtil() { * is {@code null}. If the exception is ever thrown, then that indicates that the programmer * misused the method by using it in a circumstance where its argument can be null. * - * @param ref a reference of @Nullable type + * @param the type of the reference + * @param ref a reference of @Nullable type, that is non-null at run time * @return the argument, casted to have the type qualifier @NonNull */ public static @EnsuresNonNull("#1") @NonNull T castNonNull( @@ -72,16 +72,36 @@ private NullnessUtil() { return (@NonNull T) ref; } + /** + * Suppress warnings from the Nullness Checker, with a custom error message. See {@link + * #castNonNull(Object)} for documentation. + * + * @see #castNonNull(Object) + * @param the type of the reference + * @param ref a reference of @Nullable type, that is non-null at run time + * @param message text to include if this method is misused + * @return the argument, casted to have the type qualifier @NonNull + */ + public static @EnsuresNonNull("#1") @NonNull T castNonNull( + @Nullable T ref, String message) { + assert ref != null : "Misuse of castNonNull: called with a null argument: " + message; + return (@NonNull T) ref; + } + /** * Like castNonNull, but whereas that method only checks and casts the reference itself, this * traverses all levels of the argument array. The array is recursively checked to ensure that * all elements at every array level are non-null. * + * @param the component type of the array + * @param arr an array all of whose elements, and their elements recursively, are non-null at + * run time + * @return the argument, casted to have the type qualifier @NonNull at all levels * @see #castNonNull(Object) */ public static @EnsuresNonNull("#1") @NonNull T @NonNull [] castNonNullDeep(T @Nullable [] arr) { - return (@NonNull T[]) castNonNullArray(arr); + return (@NonNull T[]) castNonNullArray(arr, null); } /** @@ -89,11 +109,32 @@ private NullnessUtil() { * traverses all levels of the argument array. The array is recursively checked to ensure that * all elements at every array level are non-null. * + * @param the component type of the array + * @param arr an array all of whose elements, and their elements recursively, are non-null at + * run time + * @param message text to include if this method is misused + * @return the argument, casted to have the type qualifier @NonNull at all levels + * @see #castNonNull(Object) + */ + public static @EnsuresNonNull("#1") + @NonNull T @NonNull [] castNonNullDeep(T @Nullable [] arr, String message) { + return (@NonNull T[]) castNonNullArray(arr, message); + } + + /** + * Like castNonNull, but whereas that method only checks and casts the reference itself, this + * traverses all levels of the argument array. The array is recursively checked to ensure that + * all elements at every array level are non-null. + * + * @param the component type of the component type of the array + * @param arr an array all of whose elements, and their elements recursively, are non-null at + * run time + * @return the argument, casted to have the type qualifier @NonNull at all levels * @see #castNonNull(Object) */ public static @EnsuresNonNull("#1") @NonNull T @NonNull [][] castNonNullDeep(T @Nullable [] @Nullable [] arr) { - return (@NonNull T[][]) castNonNullArray(arr); + return (@NonNull T[][]) castNonNullArray(arr, null); } /** @@ -101,11 +142,32 @@ private NullnessUtil() { * traverses all levels of the argument array. The array is recursively checked to ensure that * all elements at every array level are non-null. * + * @param the component type of the component type of the array + * @param arr an array all of whose elements, and their elements recursively, are non-null at + * run time + * @param message text to include if this method is misused + * @return the argument, casted to have the type qualifier @NonNull at all levels + * @see #castNonNull(Object) + */ + public static @EnsuresNonNull("#1") + @NonNull T @NonNull [][] castNonNullDeep(T @Nullable [] @Nullable [] arr, String message) { + return (@NonNull T[][]) castNonNullArray(arr, message); + } + + /** + * Like castNonNull, but whereas that method only checks and casts the reference itself, this + * traverses all levels of the argument array. The array is recursively checked to ensure that + * all elements at every array level are non-null. + * + * @param the component type (three levels in) of the array + * @param arr an array all of whose elements, and their elements recursively, are non-null at + * run time + * @return the argument, casted to have the type qualifier @NonNull at all levels * @see #castNonNull(Object) */ public static @EnsuresNonNull("#1") @NonNull T @NonNull [][][] castNonNullDeep(T @Nullable [] @Nullable [] @Nullable [] arr) { - return (@NonNull T[][][]) castNonNullArray(arr); + return (@NonNull T[][][]) castNonNullArray(arr, null); } /** @@ -113,12 +175,52 @@ private NullnessUtil() { * traverses all levels of the argument array. The array is recursively checked to ensure that * all elements at every array level are non-null. * + * @param the component type (three levels in) of the array + * @param arr an array all of whose elements, and their elements recursively, are non-null at + * run time + * @param message text to include if this method is misused + * @return the argument, casted to have the type qualifier @NonNull at all levels + * @see #castNonNull(Object) + */ + public static @EnsuresNonNull("#1") + @NonNull T @NonNull [][][] castNonNullDeep( + T @Nullable [] @Nullable [] @Nullable [] arr, String message) { + return (@NonNull T[][][]) castNonNullArray(arr, message); + } + + /** + * Like castNonNull, but whereas that method only checks and casts the reference itself, this + * traverses all levels of the argument array. The array is recursively checked to ensure that + * all elements at every array level are non-null. + * + * @param the component type of the array + * @param arr an array all of whose elements, and their elements recursively, are non-null at + * run time + * @return the argument, casted to have the type qualifier @NonNull at all levels * @see #castNonNull(Object) */ public static @EnsuresNonNull("#1") @NonNull T @NonNull [][][][] castNonNullDeep( T @Nullable [] @Nullable [] @Nullable [] @Nullable [] arr) { - return (@NonNull T[][][][]) castNonNullArray(arr); + return (@NonNull T[][][][]) castNonNullArray(arr, null); + } + + /** + * Like castNonNull, but whereas that method only checks and casts the reference itself, this + * traverses all levels of the argument array. The array is recursively checked to ensure that + * all elements at every array level are non-null. + * + * @param the component type (four levels in) of the array + * @param arr an array all of whose elements, and their elements recursively, are non-null at + * run time + * @param message text to include if this method is misused + * @return the argument, casted to have the type qualifier @NonNull at all levels + * @see #castNonNull(Object) + */ + public static @EnsuresNonNull("#1") + @NonNull T @NonNull [][][][] castNonNullDeep( + T @Nullable [] @Nullable [] @Nullable [] @Nullable [] arr, String message) { + return (@NonNull T[][][][]) castNonNullArray(arr, message); } /** @@ -126,26 +228,73 @@ private NullnessUtil() { * traverses all levels of the argument array. The array is recursively checked to ensure that * all elements at every array level are non-null. * + * @param the component type (four levels in) of the array + * @param arr an array all of whose elements, and their elements recursively, are non-null at + * run time + * @return the argument, casted to have the type qualifier @NonNull at all levels * @see #castNonNull(Object) */ public static @EnsuresNonNull("#1") @NonNull T @NonNull [][][][][] castNonNullDeep( T @Nullable [] @Nullable [] @Nullable [] @Nullable [] @Nullable [] arr) { - return (@NonNull T[][][][][]) castNonNullArray(arr); + return (@NonNull T[][][][][]) castNonNullArray(arr, null); } + /** + * Like castNonNull, but whereas that method only checks and casts the reference itself, this + * traverses all levels of the argument array. The array is recursively checked to ensure that + * all elements at every array level are non-null. + * + * @param the component type (five levels in) of the array + * @param arr an array all of whose elements, and their elements recursively, are non-null at + * run time + * @param message text to include if this method is misused + * @return the argument, casted to have the type qualifier @NonNull at all levels + * @see #castNonNull(Object) + */ + public static @EnsuresNonNull("#1") + @NonNull T @NonNull [][][][][] castNonNullDeep( + T @Nullable [] @Nullable [] @Nullable [] @Nullable [] @Nullable [] arr, + String message) { + return (@NonNull T[][][][][]) castNonNullArray(arr, message); + } + + /** + * The implementation of castNonNullDeep. + * + * @param the component type (five levels in) of the array + * @param arr an array all of whose elements, and their elements recursively, are non-null at + * run time + * @param message text to include if there is a non-null value, or null to use uncustomized + * message + * @return the argument, casted to have the type qualifier @NonNull at all levels + */ private static @NonNull T @NonNull [] castNonNullArray( - T @Nullable [] arr) { - assert arr != null : "Misuse of castNonNullArray: called with a null array argument"; + T @Nullable [] arr, @Nullable String message) { + assert arr != null + : "Misuse of castNonNullArray: called with a null array argument" + + ((message == null) ? "" : (": " + message)); for (int i = 0; i < arr.length; ++i) { - assert arr[i] != null : "Misuse of castNonNull: called with a null array element"; - checkIfArray(arr[i]); + assert arr[i] != null + : "Misuse of castNonNull: called with a null array element" + + ((message == null) ? "" : (": " + message)); + checkIfArray(arr[i], message); } return (@NonNull T[]) arr; } - private static void checkIfArray(@NonNull Object ref) { - assert ref != null : "Misuse of checkIfArray: called with a null argument"; + /** + * If the argument is an array, requires it to be non-null at all levels. + * + * @param ref a value; if an array, all of its elements, and their elements recursively, are + * non-null at run time + * @param message text to include if there is a non-null value, or null to use uncustomized + * message + */ + private static void checkIfArray(@NonNull Object ref, @Nullable String message) { + assert ref != null + : "Misuse of checkIfArray: called with a null argument" + + ((message == null) ? "" : (": " + message)); Class comp = ref.getClass().getComponentType(); if (comp != null) { // comp is non-null for arrays, otherwise null. @@ -153,7 +302,7 @@ private static void checkIfArray(@NonNull Object ref) { // Nothing to do for arrays of primitive type: primitives are // never null. } else { - castNonNullArray((Object[]) ref); + castNonNullArray((Object[]) ref, message); } } } diff --git a/checker/src/main/java/org/checkerframework/checker/nullness/NullnessVisitor.java b/checker/src/main/java/org/checkerframework/checker/nullness/NullnessVisitor.java index e72a5392f39..91ed8d9cfff 100644 --- a/checker/src/main/java/org/checkerframework/checker/nullness/NullnessVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/nullness/NullnessVisitor.java @@ -41,7 +41,11 @@ import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.common.basetype.BaseTypeChecker; +import org.checkerframework.common.basetype.BaseTypeValidator; +import org.checkerframework.common.basetype.BaseTypeVisitor; +import org.checkerframework.common.basetype.TypeValidator; import org.checkerframework.framework.flow.CFCFGBuilder; +import org.checkerframework.framework.type.AnnotatedTypeFactory; import org.checkerframework.framework.type.AnnotatedTypeMirror; import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedArrayType; import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedDeclaredType; @@ -175,7 +179,10 @@ private boolean containsSameByName( @Override protected void commonAssignmentCheck( - Tree varTree, ExpressionTree valueExp, @CompilerMessageKey String errorKey) { + Tree varTree, + ExpressionTree valueExp, + @CompilerMessageKey String errorKey, + Object... extraArgs) { // allow MonotonicNonNull to be initialized to null at declaration if (varTree.getKind() == Tree.Kind.VARIABLE) { @@ -187,20 +194,21 @@ protected void commonAssignmentCheck( return; } } - super.commonAssignmentCheck(varTree, valueExp, errorKey); + super.commonAssignmentCheck(varTree, valueExp, errorKey, extraArgs); } @Override protected void commonAssignmentCheck( AnnotatedTypeMirror varType, ExpressionTree valueExp, - @CompilerMessageKey String errorKey) { + @CompilerMessageKey String errorKey, + Object... extraArgs) { // Use the valueExp as the context because data flow will have a value for that tree. // It might not have a value for the var tree. This is sound because // if data flow has determined @PolyNull is @Nullable at the RHS, then // it is also @Nullable for the LHS. atypeFactory.replacePolyQualifier(varType, valueExp); - super.commonAssignmentCheck(varType, valueExp, errorKey); + super.commonAssignmentCheck(varType, valueExp, errorKey, extraArgs); } @Override @@ -208,7 +216,8 @@ protected void commonAssignmentCheck( AnnotatedTypeMirror varType, AnnotatedTypeMirror valueType, Tree valueTree, - @CompilerMessageKey String errorKey) { + @CompilerMessageKey String errorKey, + Object... extraArgs) { if (TypesUtils.isPrimitive(varType.getUnderlyingType()) && !TypesUtils.isPrimitive(valueType.getUnderlyingType())) { boolean succeed = checkForNullability(valueType, valueTree, UNBOXING_OF_NULLABLE); @@ -217,7 +226,7 @@ protected void commonAssignmentCheck( return; } } - super.commonAssignmentCheck(varType, valueType, valueTree, errorKey); + super.commonAssignmentCheck(varType, valueType, valueTree, errorKey, extraArgs); } /** Case 1: Check for null dereferencing. */ @@ -289,6 +298,13 @@ private static boolean isNewArrayAllZeroDims(NewArrayTree node) { return isAllZeros; } + /** + * Return true if the given node is "new X[]", in the context "toArray(new X[])". + * + * @param node a node to test + * @return true if the node is a new array within acall to toArray() + */ + @SuppressWarnings("interning:not.interned") // comparisons of Name objects private boolean isNewArrayInToArray(NewArrayTree node) { if (node.getDimensions().size() != 1) { return false; @@ -636,4 +652,39 @@ public Void visitAnnotation(AnnotationTree node, Void p) { // All annotation arguments are non-null and initialized, so no need to check them. return null; } + + @Override + protected TypeValidator createTypeValidator() { + return new NullnessValidator(checker, this, atypeFactory); + } + + /** + * Check that primitive types are annotated with {@code @NonNull} even if they are the type of a + * local variable. + */ + private static class NullnessValidator extends BaseTypeValidator { + + /** + * Create NullnessValidator. + * + * @param checker checker + * @param visitor visitor + * @param atypeFactory factory + */ + public NullnessValidator( + BaseTypeChecker checker, + BaseTypeVisitor visitor, + AnnotatedTypeFactory atypeFactory) { + super(checker, visitor, atypeFactory); + } + + @Override + protected boolean shouldCheckTopLevelDeclaredOrPrimitiveType( + AnnotatedTypeMirror type, Tree tree) { + if (type.getKind().isPrimitive()) { + return true; + } + return super.shouldCheckTopLevelDeclaredOrPrimitiveType(type, tree); + } + } } diff --git a/checker/src/main/java/org/checkerframework/checker/nullness/Opt.java b/checker/src/main/java/org/checkerframework/checker/nullness/Opt.java index 149b9f4f9eb..36a93442c0c 100644 --- a/checker/src/main/java/org/checkerframework/checker/nullness/Opt.java +++ b/checker/src/main/java/org/checkerframework/checker/nullness/Opt.java @@ -8,6 +8,7 @@ import org.checkerframework.checker.nullness.qual.EnsuresNonNullIf; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.framework.qual.AnnotatedFor; /** * Utility class for the Nullness Checker, providing every method in {@link java.util.Optional}, but @@ -21,15 +22,13 @@ * *

    import static org.checkerframework.checker.nullness.Opt.*;
    * - *

    Runtime Dependency - * - *

    Please note that using this class introduces a runtime dependency. This means that you need to - * distribute (or link to) {@code checker-qual.jar}, along with your binaries. - * - *

    To eliminate this dependency, you can simply copy this class into your own project. + *

    Runtime Dependency: If you use this class, you must distribute (or link to) {@code + * checker-qual.jar}, along with your binaries. Or, you can can copy this class into your own + * project. * * @see java.util.Optional */ +@AnnotatedFor("nullness") public final class Opt { /** The Opt class cannot be instantiated. */ @@ -40,6 +39,7 @@ private Opt() { /** * If primary is non-null, returns it, otherwise throws NoSuchElementException. * + * @param the type of the argument * @param primary a non-null value to return * @return {@code primary} if it is non-null * @throws NoSuchElementException if primary is null diff --git a/checker/src/main/java/org/checkerframework/checker/regex/RegexUtil.java b/checker/src/main/java/org/checkerframework/checker/regex/RegexUtil.java index 2269bfa4b9b..157f2cf520b 100644 --- a/checker/src/main/java/org/checkerframework/checker/regex/RegexUtil.java +++ b/checker/src/main/java/org/checkerframework/checker/regex/RegexUtil.java @@ -10,6 +10,7 @@ import org.checkerframework.checker.regex.qual.Regex; import org.checkerframework.dataflow.qual.Pure; import org.checkerframework.dataflow.qual.SideEffectFree; +import org.checkerframework.framework.qual.AnnotatedFor; import org.checkerframework.framework.qual.EnsuresQualifierIf; /** @@ -20,13 +21,12 @@ * href="https://checkerframework.org/manual/#regexutil-methods">Testing whether a string is a * regular expression in the Checker Framework manual. * - *

    Runtime Dependency: Using this class introduces a runtime dependency on the - * checker-qual package. To eliminate this dependency, you can simply copy this class into your own + *

    Runtime Dependency: If you use this class, you must distribute (or link to) {@code + * checker-qual.jar}, along with your binaries. Or, you can can copy this class into your own * project. */ -// The Purity Checker cannot show for most methods in this class that -// they are pure, even though they are. -@SuppressWarnings("all:purity") +@SuppressWarnings("allcheckers:purity") +@AnnotatedFor("nullness") public final class RegexUtil { /** This class is a collection of methods; it does not represent anything. */ 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 e3f5d8daff5..c2f947267cd 100644 --- a/checker/src/main/java/org/checkerframework/checker/signedness/SignednessAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/signedness/SignednessAnnotatedTypeFactory.java @@ -6,10 +6,14 @@ import java.lang.annotation.Annotation; import java.util.Set; import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.Element; +import javax.lang.model.type.PrimitiveType; import javax.lang.model.type.TypeKind; import javax.lang.model.type.TypeMirror; +import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.checker.signedness.qual.Signed; import org.checkerframework.checker.signedness.qual.SignedPositive; +import org.checkerframework.checker.signedness.qual.SignednessBottom; import org.checkerframework.checker.signedness.qual.SignednessGlb; import org.checkerframework.checker.signedness.qual.UnknownSignedness; import org.checkerframework.common.basetype.BaseAnnotatedTypeFactory; @@ -20,14 +24,20 @@ import org.checkerframework.common.value.qual.IntRangeFromNonNegative; import org.checkerframework.common.value.qual.IntRangeFromPositive; import org.checkerframework.common.value.util.Range; -import org.checkerframework.framework.qual.TypeUseLocation; import org.checkerframework.framework.type.AnnotatedTypeFactory; import org.checkerframework.framework.type.AnnotatedTypeMirror; +import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedDeclaredType; +import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedPrimitiveType; +import org.checkerframework.framework.type.DefaultTypeHierarchy; +import org.checkerframework.framework.type.QualifierHierarchy; +import org.checkerframework.framework.type.TypeHierarchy; import org.checkerframework.framework.type.treeannotator.ListTreeAnnotator; import org.checkerframework.framework.type.treeannotator.PropagationTreeAnnotator; import org.checkerframework.framework.type.treeannotator.TreeAnnotator; -import org.checkerframework.framework.util.defaults.QualifierDefaults; import org.checkerframework.javacutil.AnnotationBuilder; +import org.checkerframework.javacutil.AnnotationUtils; +import org.checkerframework.javacutil.BugInCF; +import org.checkerframework.javacutil.TypesUtils; /** * The type factory for the Signedness Checker. @@ -36,14 +46,17 @@ */ public class SignednessAnnotatedTypeFactory extends BaseAnnotatedTypeFactory { - /** The @SignednessGlb annotation. */ - private final AnnotationMirror SIGNEDNESS_GLB = - AnnotationBuilder.fromClass(elements, SignednessGlb.class); - /** The @Signed annotation. */ - private final AnnotationMirror SIGNED = AnnotationBuilder.fromClass(elements, Signed.class); /** The @UnknownSignedness annotation. */ private final AnnotationMirror UNKNOWN_SIGNEDNESS = AnnotationBuilder.fromClass(elements, UnknownSignedness.class); + /** The @Signed annotation. */ + private final AnnotationMirror SIGNED = AnnotationBuilder.fromClass(elements, Signed.class); + /** The @SignednessGlb annotation. */ + private final AnnotationMirror SIGNEDNESS_GLB = + AnnotationBuilder.fromClass(elements, SignednessGlb.class); + /** The @SignednessBottom annotation. */ + private final AnnotationMirror SIGNEDNESS_BOTTOM = + AnnotationBuilder.fromClass(elements, SignednessBottom.class); /** The @NonNegative annotation of the Index Checker, as represented by the Value Checker. */ private final AnnotationMirror INT_RANGE_FROM_NON_NEGATIVE = @@ -73,12 +86,6 @@ protected Set> createSupportedTypeQualifiers() { @Override protected void addComputedTypeAnnotations( Tree tree, AnnotatedTypeMirror type, boolean iUseFlow) { - // Prevent @ImplicitFor from applying to local variables of type byte, short, int, and long, - // but adding the top type to them, which permits flow-sensitive type refinement. - // (When it is possible to default types based on their TypeKinds, - // this whole method will no longer be needed.) - addUnknownSignednessToSomeLocals(tree, type); - if (!computingAnnotatedTypeMirrorOfLHS) { addSignednessGlbAnnotation(tree, type); } @@ -162,25 +169,6 @@ private void addSignednessGlbAnnotation(Tree tree, AnnotatedTypeMirror type) { } } - /** - * If the tree is a local variable and the type is byte, short, int, or long, then add the - * UnknownSignedness annotation so that dataflow can refine it. - */ - private void addUnknownSignednessToSomeLocals(Tree tree, AnnotatedTypeMirror type) { - switch (type.getKind()) { - case BYTE: - case SHORT: - case INT: - case LONG: - QualifierDefaults defaults = new QualifierDefaults(elements, this); - defaults.addCheckedCodeDefault(UNKNOWN_SIGNEDNESS, TypeUseLocation.LOCAL_VARIABLE); - defaults.annotate(tree, type); - break; - default: - // Nothing for other cases. - } - } - @Override protected TreeAnnotator createTreeAnnotator() { return new ListTreeAnnotator( @@ -188,10 +176,14 @@ protected TreeAnnotator createTreeAnnotator() { } /** - * This TreeAnnotator ensures that boolean expressions are not given Unsigned or Signed - * annotations by {@link PropagationTreeAnnotator}, that shift results take on the type of their - * left operand, and that the types of identifiers are refined based on the results of the Value - * Checker. + * This TreeAnnotator ensures that: + * + *

      + *
    • boolean expressions are not given Unsigned or Signed annotations by {@link + * PropagationTreeAnnotator}, + *
    • shift results take on the type of their left operand, + *
    • the types of identifiers are refined based on the results of the Value Checker. + *
    */ private class SignednessTreeAnnotator extends TreeAnnotator { @@ -235,4 +227,153 @@ public Void visitCompoundAssignment(CompoundAssignmentTree tree, AnnotatedTypeMi return null; } } + + @Override + protected void addAnnotationsFromDefaultForType( + @Nullable Element element, AnnotatedTypeMirror type) { + if (TypesUtils.isFloating(type.getUnderlyingType()) + || TypesUtils.isBoxedFloating(type.getUnderlyingType()) + || type.getKind() == TypeKind.CHAR + || TypesUtils.isDeclaredOfName(type.getUnderlyingType(), "java.lang.Character")) { + // Floats are always signed and chars are always unsigned. + super.addAnnotationsFromDefaultForType(null, type); + } else { + super.addAnnotationsFromDefaultForType(element, type); + } + } + + @Override + protected TypeHierarchy createTypeHierarchy() { + return new SignednessTypeHierarchy( + checker, + getQualifierHierarchy(), + checker.getBooleanOption("ignoreRawTypeArguments", true), + checker.hasOption("invariantArrays")); + } + + /** + * The type hierarchy for the signedness type system. If A is narrower (fewer bits) than B, then + * A with any qualifier is a subtype of @SignedPositive B. + */ + protected class SignednessTypeHierarchy extends DefaultTypeHierarchy { + + /** + * Create a new SignednessTypeHierarchy. + * + * @param checker the checker + * @param qualifierHierarchy the qualifier hierarchy + * @param ignoreRawTypes from -AignoreRawTypes + * @param invariantArrayComponents from -AinvariantArrays + */ + public SignednessTypeHierarchy( + BaseTypeChecker checker, + QualifierHierarchy qualifierHierarchy, + boolean ignoreRawTypes, + boolean invariantArrayComponents) { + super(checker, qualifierHierarchy, ignoreRawTypes, invariantArrayComponents); + } + + @Override + public Boolean visitPrimitive_Primitive( + AnnotatedPrimitiveType subtype, AnnotatedPrimitiveType supertype, Void p) { + + boolean superResult = super.visitPrimitive_Primitive(subtype, supertype, p); + if (superResult) { + return true; + } + + PrimitiveType subPrimitive = subtype.getUnderlyingType(); + PrimitiveType superPrimitive = supertype.getUnderlyingType(); + if (isNarrowerIntegral(subPrimitive, superPrimitive)) { + AnnotationMirror superAnno = supertype.getAnnotationInHierarchy(UNKNOWN_SIGNEDNESS); + if (!AnnotationUtils.areSameByName(superAnno, SIGNEDNESS_BOTTOM)) { + return true; + } + } + + return false; + } + + @Override + public Boolean visitPrimitive_Declared( + AnnotatedPrimitiveType subtype, AnnotatedDeclaredType supertype, Void p) { + boolean superBoxed = TypesUtils.isBoxedPrimitive(supertype.getUnderlyingType()); + if (superBoxed) { + return visitPrimitive_Primitive(subtype, getUnboxedType(supertype), p); + } + return super.visitPrimitive_Declared(subtype, supertype, p); + } + + @Override + public Boolean visitDeclared_Declared( + AnnotatedDeclaredType subtype, AnnotatedDeclaredType supertype, Void p) { + boolean subBoxed = TypesUtils.isBoxedPrimitive(subtype.getUnderlyingType()); + if (subBoxed) { + boolean superBoxed = TypesUtils.isBoxedPrimitive(supertype.getUnderlyingType()); + if (superBoxed) { + return visitPrimitive_Primitive( + getUnboxedType(subtype), getUnboxedType(supertype), p); + } + } + return super.visitDeclared_Declared(subtype, supertype, p); + } + + @Override + public Boolean visitDeclared_Primitive( + AnnotatedDeclaredType subtype, AnnotatedPrimitiveType supertype, Void p) { + boolean subBoxed = TypesUtils.isBoxedPrimitive(subtype.getUnderlyingType()); + if (subBoxed) { + return visitPrimitive_Primitive(getUnboxedType(subtype), supertype, p); + } + return super.visitDeclared_Primitive(subtype, supertype, p); + } + + /** + * Returns true if both types are integral and the first type is strictly narrower + * (represented by fewer bits) than the second type. + * + * @param a a primitive type + * @param b a primitive type + * @return true if {@code a} is represented by fewer bits than {@code b} + */ + private boolean isNarrowerIntegral(PrimitiveType a, PrimitiveType b) { + int aBits = numIntegralBits(a); + if (aBits == -1) { + return false; + } + int bBits = numIntegralBits(b); + if (bBits == -1) { + return false; + } + return aBits < bBits; + } + + /** + * Returns the number of bits in the representation of an integral primitive type. Returns + * -1 if the type is not an integral primitive type. + * + * @param p a primitive type + * @return the number of bits in its representation, or -1 if not integral + */ + private int numIntegralBits(PrimitiveType p) { + switch (p.getKind()) { + case BYTE: + return 8; + case SHORT: + return 16; + case CHAR: + return 16; + case INT: + return 32; + case LONG: + return 64; + case BOOLEAN: + case DOUBLE: + case FLOAT: + return -1; + default: + throw new BugInCF("Unexpected primitive type " + p); + } + } + } } diff --git a/checker/src/main/java/org/checkerframework/checker/signedness/SignednessUtil.java b/checker/src/main/java/org/checkerframework/checker/signedness/SignednessUtil.java index 6a846d2f4c1..948ab11dcbd 100644 --- a/checker/src/main/java/org/checkerframework/checker/signedness/SignednessUtil.java +++ b/checker/src/main/java/org/checkerframework/checker/signedness/SignednessUtil.java @@ -6,6 +6,7 @@ import java.nio.ByteBuffer; import java.nio.IntBuffer; import org.checkerframework.checker.signedness.qual.Unsigned; +import org.checkerframework.framework.qual.AnnotatedFor; /** * Provides static utility methods for unsigned values. Most of these re-implement functionality @@ -15,6 +16,7 @@ * * @checker_framework.manual #signedness-utilities Utility routines for manipulating unsigned values */ +@AnnotatedFor("nullness") public final class SignednessUtil { private SignednessUtil() { diff --git a/checker/src/main/java/org/checkerframework/checker/signedness/SignednessUtilExtra.java b/checker/src/main/java/org/checkerframework/checker/signedness/SignednessUtilExtra.java index 464e37c50ae..6b38b1d5202 100644 --- a/checker/src/main/java/org/checkerframework/checker/signedness/SignednessUtilExtra.java +++ b/checker/src/main/java/org/checkerframework/checker/signedness/SignednessUtilExtra.java @@ -3,6 +3,7 @@ import java.awt.Dimension; import java.awt.image.BufferedImage; import org.checkerframework.checker.signedness.qual.Unsigned; +import org.checkerframework.framework.qual.AnnotatedFor; /** * Provides more static utility methods for unsigned values. These methods use Java packages not @@ -10,7 +11,9 @@ * * @checker_framework.manual #signedness-utilities Utility routines for manipulating unsigned values */ +@AnnotatedFor("nullness") public class SignednessUtilExtra { + /** Do not instantiate this class. */ private SignednessUtilExtra() { throw new Error("Do not instantiate"); } diff --git a/checker/src/main/java/org/checkerframework/checker/signedness/SignednessVisitor.java b/checker/src/main/java/org/checkerframework/checker/signedness/SignednessVisitor.java index d2b3687d6d8..cef73f24b05 100644 --- a/checker/src/main/java/org/checkerframework/checker/signedness/SignednessVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/signedness/SignednessVisitor.java @@ -10,6 +10,7 @@ import com.sun.source.tree.Tree.Kind; import com.sun.source.tree.TypeCastTree; import javax.lang.model.type.TypeKind; +import org.checkerframework.checker.interning.qual.InternedDistinct; import org.checkerframework.checker.signedness.qual.PolySigned; import org.checkerframework.checker.signedness.qual.Signed; import org.checkerframework.checker.signedness.qual.Unsigned; @@ -212,7 +213,8 @@ private boolean isMaskedShiftEitherSignedness(BinaryTree shiftExpr) { // enclosing immediately contains shiftExpr or a parenthesized version of shiftExpr Tree enclosing = enclosingPair.first; // enclosingChild is a child of enclosing: shiftExpr or a parenthesized version of it. - Tree enclosingChild = enclosingPair.second; + @SuppressWarnings("interning:assignment.type.incompatible") // comparing AST nodes + @InternedDistinct Tree enclosingChild = enclosingPair.second; if (!isMask(enclosing)) { return false; diff --git a/checker/src/main/java/org/checkerframework/checker/signedness/qual/Signed.java b/checker/src/main/java/org/checkerframework/checker/signedness/qual/Signed.java index 3af02116d87..377e79391ac 100644 --- a/checker/src/main/java/org/checkerframework/checker/signedness/qual/Signed.java +++ b/checker/src/main/java/org/checkerframework/checker/signedness/qual/Signed.java @@ -8,6 +8,7 @@ import org.checkerframework.framework.qual.DefaultFor; import org.checkerframework.framework.qual.SubtypeOf; import org.checkerframework.framework.qual.TypeKind; +import org.checkerframework.framework.qual.UpperBoundFor; /** * The value is to be interpreted as signed. That is, if the most significant bit in the bitwise @@ -26,18 +27,17 @@ TypeKind.LONG, TypeKind.SHORT, TypeKind.FLOAT, - TypeKind.DOUBLE, - TypeKind.CHAR - } - - // This is commented out until implicitly signed boxed types are implemented - // correctly. - - /*, + TypeKind.DOUBLE + }, types = { java.lang.Byte.class, - java.lang.Short.class, java.lang.Integer.class, - java.lang.Long.class - }*/ ) + java.lang.Long.class, + java.lang.Short.class, + java.lang.Float.class, + java.lang.Double.class + }) +@UpperBoundFor( + typeKinds = {TypeKind.FLOAT, TypeKind.DOUBLE}, + types = {java.lang.Float.class, java.lang.Double.class}) public @interface Signed {} diff --git a/checker/src/main/java/org/checkerframework/checker/signedness/qual/Unsigned.java b/checker/src/main/java/org/checkerframework/checker/signedness/qual/Unsigned.java index be27a217a1a..3b8029b6996 100644 --- a/checker/src/main/java/org/checkerframework/checker/signedness/qual/Unsigned.java +++ b/checker/src/main/java/org/checkerframework/checker/signedness/qual/Unsigned.java @@ -5,7 +5,10 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import org.checkerframework.framework.qual.DefaultFor; import org.checkerframework.framework.qual.SubtypeOf; +import org.checkerframework.framework.qual.TypeKind; +import org.checkerframework.framework.qual.UpperBoundFor; /** * The value is to be interpreted as unsigned. That is, if the most significant bit in the bitwise @@ -18,4 +21,10 @@ @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) @SubtypeOf({UnknownSignedness.class}) +@DefaultFor( + typeKinds = {TypeKind.CHAR}, + types = {java.lang.Character.class}) +@UpperBoundFor( + typeKinds = {TypeKind.CHAR}, + types = {java.lang.Character.class}) public @interface Unsigned {} diff --git a/checker/src/main/java/org/checkerframework/checker/units/UnitsTools.java b/checker/src/main/java/org/checkerframework/checker/units/UnitsTools.java index 2559f5cf923..56f128df42c 100644 --- a/checker/src/main/java/org/checkerframework/checker/units/UnitsTools.java +++ b/checker/src/main/java/org/checkerframework/checker/units/UnitsTools.java @@ -21,10 +21,13 @@ import org.checkerframework.checker.units.qual.mol; import org.checkerframework.checker.units.qual.radians; import org.checkerframework.checker.units.qual.s; +import org.checkerframework.framework.qual.AnnotatedFor; + +// TODO: add fromTo methods for all useful unit combinations. /** Utility methods to generate annotated types and to convert between them. */ @SuppressWarnings({"units", "checkstyle:constantname"}) -// TODO: add fromTo methods for all useful unit combinations. +@AnnotatedFor("nullness") public class UnitsTools { // Acceleration public static final @mPERs2 int mPERs2 = 1; diff --git a/checker/src/test/java/tests/FormatterUnitTest.java b/checker/src/test/java/tests/FormatterUnitTest.java new file mode 100644 index 00000000000..f80ae1c895e --- /dev/null +++ b/checker/src/test/java/tests/FormatterUnitTest.java @@ -0,0 +1,45 @@ +package tests; + +import org.checkerframework.checker.formatter.FormatUtil; +import org.junit.Assert; +import org.junit.Test; + +public class FormatterUnitTest { + + @SuppressWarnings("deprecation") // calls methods that are used only for testing + @Test + public void testConversionCharFromFormat() { + Assert.assertEquals('s', FormatUtil.conversionCharFromFormat("%1$2s")); + Assert.assertEquals('s', FormatUtil.conversionCharFromFormat("%1$s")); + Assert.assertEquals('t', FormatUtil.conversionCharFromFormat("%1$tb")); + Assert.assertEquals('t', FormatUtil.conversionCharFromFormat("%1$te")); + Assert.assertEquals('t', FormatUtil.conversionCharFromFormat("%1$tm")); + Assert.assertEquals('t', FormatUtil.conversionCharFromFormat("%1$tY")); + Assert.assertEquals('f', FormatUtil.conversionCharFromFormat("%+10.4f")); + Assert.assertEquals('s', FormatUtil.conversionCharFromFormat("%2$2s")); + Assert.assertEquals('s', FormatUtil.conversionCharFromFormat("%2$s")); + Assert.assertEquals('f', FormatUtil.conversionCharFromFormat("%(,.2f")); + Assert.assertEquals('s', FormatUtil.conversionCharFromFormat("%3$2s")); + Assert.assertEquals('s', FormatUtil.conversionCharFromFormat("%3$s")); + Assert.assertEquals('s', FormatUtil.conversionCharFromFormat("%4$2s")); + Assert.assertEquals('s', FormatUtil.conversionCharFromFormat("%4$s")); + Assert.assertEquals('s', FormatUtil.conversionCharFromFormat("% { @@ -29,6 +31,80 @@ public boolean equals(Object o) { return false; } + @EqualsMethod + @org.checkerframework.dataflow.qual.Pure + public boolean equals2(Object o) { + // Using == is OK if it's the first statement in the equals method + // and it compares "this" against the argument. + if (this == o) { + return true; + } + // Not the first statement in the method. + // :: error: (not.interned) + if (o == this) { + return true; + } + return false; + } + + @org.checkerframework.dataflow.qual.Pure + public boolean equals3(Object o) { + // Not equals() or annotated as @EqualsMethod. + // :: error: (not.interned) + if (this == o) { + return true; + } + // Not the first statement in the method. + // :: error: (not.interned) + if (o == this) { + return true; + } + return false; + } + + @EqualsMethod + @org.checkerframework.dataflow.qual.Pure + public static boolean equals4(Object thisOne, Object o) { + // Using == is OK if it's the first statement in the equals method + // and it compares "this" against the argument. + if (thisOne == o) { + return true; + } + // Not the first statement in the method. + // :: error: (not.interned) + if (o == thisOne) { + return true; + } + return false; + } + + @org.checkerframework.dataflow.qual.Pure + public static boolean equals5(Object thisOne, Object o) { + // Not equals() or annotated as @EqualsMethod. + // :: error: (not.interned) + if (thisOne == o) { + return true; + } + // Not the first statement in the method. + // :: error: (not.interned) + if (o == thisOne) { + return true; + } + return false; + } + + @EqualsMethod + // :: error: (invalid.method.annotation) + public boolean equals6() { + return true; + } + + @EqualsMethod + // :: error: (invalid.method.annotation) + public boolean equals7(int a, int b, int c) { + return true; + } + @Override @org.checkerframework.dataflow.qual.Pure public int compareTo(HeuristicsTest o) { @@ -46,6 +122,82 @@ public int compareTo(HeuristicsTest o) { return 0; } + @CompareToMethod + @org.checkerframework.dataflow.qual.Pure + public int compareTo2(HeuristicsTest o) { + // Using == is OK if it's the first statement in the equals method + // and it compares "this" against the argument. + + if (o == this) { + return 0; + } + // Not the first statement in the method. + // :: error: (not.interned) + if (this == o) { + return 0; + } + return 0; + } + + @org.checkerframework.dataflow.qual.Pure + public int compareTo3(HeuristicsTest o) { + // Not compareTo or annotated as @CompareToMethod + // :: error: (not.interned) + if (o == this) { + return 0; + } + // Not the first statement in the method. + // :: error: (not.interned) + if (this == o) { + return 0; + } + return 0; + } + + @CompareToMethod + @org.checkerframework.dataflow.qual.Pure + public static int compareTo4(HeuristicsTest thisOne, HeuristicsTest o) { + // Using == is OK if it's the first statement in the equals method + // and it compares "this" against the argument. + + if (o == thisOne) { + return 0; + } + // Not the first statement in the method. + // :: error: (not.interned) + if (thisOne == o) { + return 0; + } + return 0; + } + + @org.checkerframework.dataflow.qual.Pure + public static int compareTo5(HeuristicsTest thisOne, HeuristicsTest o) { + // Not compareTo or annotated as @CompareToMethod + // :: error: (not.interned) + if (o == thisOne) { + return 0; + } + // Not the first statement in the method. + // :: error: (not.interned) + if (thisOne == o) { + return 0; + } + return 0; + } + + @EqualsMethod + // :: error: (invalid.method.annotation) + public boolean compareTo6() { + return true; + } + + @EqualsMethod + // :: error: (invalid.method.annotation) + public boolean compareTo7(int a, int b, int c) { + return true; + } + public boolean optimizeEqualsClient(Object a, Object b, Object[] arr) { // Using == is OK if it's the left-hand side of an || whose right-hand // side is a call to equals with the same arguments. diff --git a/checker/tests/interning/ThreadUsesObjectEquals.java b/checker/tests/interning/ThreadUsesObjectEquals.java new file mode 100644 index 00000000000..e494d1fd364 --- /dev/null +++ b/checker/tests/interning/ThreadUsesObjectEquals.java @@ -0,0 +1,5 @@ +public class ThreadUsesObjectEquals { + boolean p(Thread a, Thread b) { + return a == b; + } +} diff --git a/checker/tests/interning/UsesObjectEqualsTest.java b/checker/tests/interning/UsesObjectEqualsTest.java index 883c6ef0d3f..89cb2b297b6 100644 --- a/checker/tests/interning/UsesObjectEqualsTest.java +++ b/checker/tests/interning/UsesObjectEqualsTest.java @@ -20,6 +20,22 @@ public boolean equals(Object o) { } } + @UsesObjectEquals + class B3 extends A { + @Override + public boolean equals(Object o3) { + return this == o3; + } + } + + @UsesObjectEquals + class B4 extends A { + @Override + public boolean equals(Object o4) { + return o4 == this; + } + } + // changed to inherited, no (superclass.annotated) warning class C extends A {} @@ -60,4 +76,19 @@ class ExtendsInner1 extends UsesObjectEqualsTest.A {} class ExtendsInner2 extends UsesObjectEqualsTest.A {} class MyList extends LinkedList {} + + class DoesNotUseObjectEquals { + @Override + public boolean equals(Object o) { + return super.equals(o); + } + } + + @UsesObjectEquals + class SubclassUsesObjectEquals extends DoesNotUseObjectEquals { + @Override + public boolean equals(Object o) { + return this == o; + } + } } diff --git a/checker/tests/lock/ChapterExamples.java b/checker/tests/lock/ChapterExamples.java index 20305c020f8..cdd316541ff 100644 --- a/checker/tests/lock/ChapterExamples.java +++ b/checker/tests/lock/ChapterExamples.java @@ -442,7 +442,6 @@ void unboxing() { @GuardedBy("lock") Integer b = 1; int d; synchronized (lock) { - // :: error: (assignment.type.incompatible) d = b; // Expected, since b cannot be @GuardedBy("lock") since it is a boxed primitive. @@ -464,7 +463,6 @@ void unboxing() { c = new Integer(c.intValue() + b.intValue()); // The de-sugared version } - // :: error: (assignment.type.incompatible) a = b; b = c; // OK } diff --git a/checker/tests/lock/Strings.java b/checker/tests/lock/Strings.java index 1f021ef5b14..568f4a8b51c 100644 --- a/checker/tests/lock/Strings.java +++ b/checker/tests/lock/Strings.java @@ -8,17 +8,14 @@ public class Strings { final Object lock = new Object(); - // Tests that @GuardedBy({}) is @ImplicitFor(typeNames = { java.lang.String.class }) + // These casts are safe because if the casted Object is a String, it must be @GuardedBy({}) void StringIsGBnothing( @GuardedByUnknown Object o1, @GuardedBy("lock") Object o2, @GuardSatisfied Object o3, @GuardedByBottom Object o4) { - // :: error: (assignment.type.incompatible) String s1 = (String) o1; - // :: error: (assignment.type.incompatible) String s2 = (String) o2; - // :: error: (assignment.type.incompatible) String s3 = (String) o3; String s4 = (String) o4; // OK } diff --git a/checker/tests/nullness-safedefaultssourcecode/Issue3449.java b/checker/tests/nullness-safedefaultssourcecode/Issue3449.java new file mode 100644 index 00000000000..417721ecb0a --- /dev/null +++ b/checker/tests/nullness-safedefaultssourcecode/Issue3449.java @@ -0,0 +1,15 @@ +// Test case for https://tinyurl.com/cfissue/3449 + +import org.checkerframework.framework.qual.AnnotatedFor; + +@AnnotatedFor("nullness") +public class Issue3449 { + + int length; + Object[] objs; + + public Issue3449(Object... args) { + length = args.length; + objs = args; + } +} diff --git a/checker/tests/nullness/Issue3443.java b/checker/tests/nullness/Issue3443.java new file mode 100644 index 00000000000..d70d281fb3e --- /dev/null +++ b/checker/tests/nullness/Issue3443.java @@ -0,0 +1,19 @@ +import org.checkerframework.checker.nullness.qual.Nullable; + +class Issue3443 { + static > Supplier3443 passThrough(T t) { + // :: error: (return.type.incompatible) + return t; + } + + public static void main(String[] args) { + Supplier3443<@Nullable String> s1 = () -> null; + // TODO: passThrough(s1) should cause an error. #979. + Supplier3443 s2 = passThrough(s1); + s2.get().toString(); + } +} + +interface Supplier3443 { + T get(); +} diff --git a/checker/tests/nullness/java8/Issue1098.java b/checker/tests/nullness/java8/Issue1098.java index f37c81f7d71..aa63d5d92f2 100644 --- a/checker/tests/nullness/java8/Issue1098.java +++ b/checker/tests/nullness/java8/Issue1098.java @@ -10,7 +10,7 @@ void cls(Class p1, T p2) {} void use() { opt(Optional.empty(), null); - // TODO: false positive, because type agrument inference does not account for @Covariant. + // TODO: false positive, because type argument inference does not account for @Covariant. // See https://github.com/typetools/checker-framework/issues/979. // :: error: (argument.type.incompatible) cls(this.getClass(), null); diff --git a/checker/tests/nullness/java8/Issue1098NoJdk.java b/checker/tests/nullness/java8/Issue1098NoJdk.java index 54265353cdb..da7c3ee4f4d 100644 --- a/checker/tests/nullness/java8/Issue1098NoJdk.java +++ b/checker/tests/nullness/java8/Issue1098NoJdk.java @@ -12,7 +12,7 @@ class Issue1098NoJdk { void cls2(Class p1, T p2) {} void use2(MyObject ths) { - // TODO: false positive, because type agrument inference does not account for @Covariant. + // TODO: false positive, because type argument inference does not account for @Covariant. // See https://github.com/typetools/checker-framework/issues/979. // :: error: (argument.type.incompatible) cls2(ths.getMyClass(), null); diff --git a/checker/tests/nullness/java8/Issue1633.java b/checker/tests/nullness/java8/Issue1633.java index 14bf178aa36..48eb06f1e3e 100644 --- a/checker/tests/nullness/java8/Issue1633.java +++ b/checker/tests/nullness/java8/Issue1633.java @@ -53,7 +53,7 @@ void foo5(Optional1633 o, Supplier<@NonNull String> supplyNonNull) { } void foo6(Optional1633 o, Supplier<@NonNull String> supplyNonNull) { - // :: error: (argument.type.incompatible) :: error: (assignment.type.incompatible) + // :: error: (assignment.type.incompatible) @NonNull String str1 = o.orElseGetNullable(supplyNonNull); } diff --git a/checker/tests/signedness/Arrays.java b/checker/tests/signedness/Arrays.java new file mode 100644 index 00000000000..c60c1966c6e --- /dev/null +++ b/checker/tests/signedness/Arrays.java @@ -0,0 +1,5 @@ +public class Arrays { + void test() { + Object[] os = new Double[234]; + } +} diff --git a/checker/tests/signedness/BoxedPrimitives.java b/checker/tests/signedness/BoxedPrimitives.java new file mode 100644 index 00000000000..010d7d2c101 --- /dev/null +++ b/checker/tests/signedness/BoxedPrimitives.java @@ -0,0 +1,83 @@ +import java.util.LinkedList; +import org.checkerframework.checker.signedness.qual.Signed; +import org.checkerframework.checker.signedness.qual.Unsigned; + +public class BoxedPrimitives { + + @Signed int si; + @Unsigned int ui; + + @Signed Integer sbi; + @Unsigned Integer ubi; + + void argSigned(@Signed int x) { + si = x; + sbi = x; + // :: error: (assignment.type.incompatible) + ui = x; + // :: error: (assignment.type.incompatible) + ubi = x; + } + + void argUnsigned(@Unsigned int x) { + // :: error: (assignment.type.incompatible) + si = x; + // :: error: (assignment.type.incompatible) + sbi = x; + ui = x; + ubi = x; + } + + void argSignedBoxed(@Signed Integer x) { + si = x; + sbi = x; + // :: error: (assignment.type.incompatible) + ui = x; + // :: error: (assignment.type.incompatible) + ubi = x; + } + + void argUnsignedBoxed(@Unsigned Integer x) { + // :: error: (assignment.type.incompatible) + si = x; + // :: error: (assignment.type.incompatible) + sbi = x; + ui = x; + ubi = x; + } + + void client() { + argSigned(si); + argSignedBoxed(si); + argSigned(sbi); + argSignedBoxed(sbi); + // :: error: (argument.type.incompatible) + argUnsigned(si); + // :: error: (argument.type.incompatible) + argUnsignedBoxed(si); + // :: error: (argument.type.incompatible) + argUnsigned(sbi); + // :: error: (argument.type.incompatible) + argUnsignedBoxed(sbi); + // :: error: (argument.type.incompatible) + argSigned(ui); + // :: error: (argument.type.incompatible) + argSignedBoxed(ui); + // :: error: (argument.type.incompatible) + argSigned(ubi); + // :: error: (argument.type.incompatible) + argSignedBoxed(ubi); + argUnsigned(ui); + argUnsignedBoxed(ui); + argUnsigned(ubi); + argUnsignedBoxed(ubi); + } + + public LinkedList commands; + + void forLoop() { + for (Integer ix : this.commands) { + argSigned(ix); + } + } +} diff --git a/checker/tests/signedness/CastedShifts.java b/checker/tests/signedness/CastedShifts.java index 89dfd16db88..c849822e3bd 100644 --- a/checker/tests/signedness/CastedShifts.java +++ b/checker/tests/signedness/CastedShifts.java @@ -36,19 +36,15 @@ public void CastedIntShifts(@Unsigned int unsigned, @Signed int signed) { byteRes = (@Signed byte) (signed >> 0); // Cast to char. - @UnknownSignedness char charRes; + char charRes; // Shifting right by 23, the introduced bits are cast away charRes = (@Unsigned char) (unsigned >>> 23); charRes = (@Unsigned char) (unsigned >> 23); - charRes = (@Signed char) (signed >>> 23); - charRes = (@Signed char) (signed >> 23); // Shifting right by 24, the introduced bits are still cast away. charRes = (@Unsigned char) (unsigned >>> 24); charRes = (@Unsigned char) (unsigned >> 24); - charRes = (@Signed char) (signed >>> 24); - charRes = (@Signed char) (signed >> 24); // Shifting right by 25, now the MSB matters. charRes = (@Unsigned char) (unsigned >>> 25); @@ -56,15 +52,9 @@ public void CastedIntShifts(@Unsigned int unsigned, @Signed int signed) { // :: error: (shift.signed) charRes = (@Unsigned char) (unsigned >> 25); - // :: error: (shift.unsigned) - charRes = (@Signed char) (signed >>> 25); - charRes = (@Signed char) (signed >> 25); - // Shifting right by zero should behave as assignment charRes = (@Unsigned char) (unsigned >>> 0); charRes = (@Unsigned char) (unsigned >> 0); - charRes = (@Signed char) (signed >>> 0); - charRes = (@Signed char) (signed >> 0); // Cast to short. @UnknownSignedness short shortRes; @@ -235,19 +225,15 @@ public void CastedLongShifts(@Unsigned long unsigned, @Signed long signed) { byteRes = (@Signed byte) (signed >> 0); // Cast to char. - @UnknownSignedness char charRes; + char charRes; // Shifting right by 55, the introduced bits are cast away charRes = (@Unsigned char) (unsigned >>> 55); charRes = (@Unsigned char) (unsigned >> 55); - charRes = (@Signed char) (signed >>> 55); - charRes = (@Signed char) (signed >> 55); // Shifting right by 56, the introduced bits are still cast away. charRes = (@Unsigned char) (unsigned >>> 56); charRes = (@Unsigned char) (unsigned >> 56); - charRes = (@Signed char) (signed >>> 56); - charRes = (@Signed char) (signed >> 56); // Shifting right by 57, now the MSB matters. charRes = (@Unsigned char) (unsigned >>> 57); @@ -255,15 +241,9 @@ public void CastedLongShifts(@Unsigned long unsigned, @Signed long signed) { // :: error: (shift.signed) charRes = (@Unsigned char) (unsigned >> 57); - // :: error: (shift.unsigned) - charRes = (@Signed char) (signed >>> 57); - charRes = (@Signed char) (signed >> 57); - // Shifting right by zero should behave as assignment charRes = (@Unsigned char) (unsigned >>> 0); charRes = (@Unsigned char) (unsigned >> 0); - charRes = (@Signed char) (signed >>> 0); - charRes = (@Signed char) (signed >> 0); // Cast to short. @UnknownSignedness short shortRes; diff --git a/checker/tests/signedness/CompoundAssignments.java b/checker/tests/signedness/CompoundAssignmentsSignedness.java similarity index 99% rename from checker/tests/signedness/CompoundAssignments.java rename to checker/tests/signedness/CompoundAssignmentsSignedness.java index ebbc8074967..62dccb004a1 100644 --- a/checker/tests/signedness/CompoundAssignments.java +++ b/checker/tests/signedness/CompoundAssignmentsSignedness.java @@ -1,6 +1,6 @@ import org.checkerframework.checker.signedness.qual.*; -public class CompoundAssignments { +public class CompoundAssignmentsSignedness { public void DivModTest( @Unsigned int unsigned, diff --git a/checker/tests/signedness/DefaultsSignedness.java b/checker/tests/signedness/DefaultsSignedness.java index 518ac2465ce..c91d9cb4ddb 100644 --- a/checker/tests/signedness/DefaultsSignedness.java +++ b/checker/tests/signedness/DefaultsSignedness.java @@ -112,31 +112,14 @@ public void SignedTest( // Test floats @Signed float sinFloat; - @SignednessGlb float conFloat; sinFloat = testFloat; - // :: error: (assignment.type.incompatible) - conFloat = testFloat; - // Test doubles @Signed double sinDouble; - @SignednessGlb double conDouble; sinDouble = testDouble; - // :: error: (assignment.type.incompatible) - conDouble = testDouble; - - // Test chars - @Signed char sinChar; - @SignednessGlb char conChar; - - sinChar = testChar; - - // :: error: (assignment.type.incompatible) - conChar = testChar; - /* // Test boxed bytes @Signed Byte sinBoxedByte; diff --git a/checker/tests/signedness/Issue2483.java b/checker/tests/signedness/Issue2483.java new file mode 100644 index 00000000000..9217ca9c1ae --- /dev/null +++ b/checker/tests/signedness/Issue2483.java @@ -0,0 +1,8 @@ +import org.checkerframework.checker.signedness.qual.*; + +class Issue2483 { + void foo(String a, byte[] b) { + @Unsigned int len = a.length(); + @Unsigned int len2 = b.length; + } +} diff --git a/checker/tests/signedness/LocalVarDefaults.java b/checker/tests/signedness/LocalVarDefaults.java new file mode 100644 index 00000000000..e80bd610046 --- /dev/null +++ b/checker/tests/signedness/LocalVarDefaults.java @@ -0,0 +1,29 @@ +import org.checkerframework.checker.signedness.qual.Signed; +import org.checkerframework.checker.signedness.qual.Unsigned; + +public class LocalVarDefaults { + + void methodInt(@Unsigned int unsignedInt, @Signed int signedInt) { + int local = unsignedInt; + int local2 = signedInt; + } + + // :: error: (type.invalid.annotations.on.use) + void methodDouble(@Unsigned double unsigned, @Signed double signed) { + // :: error: (assignment.type.incompatible) + double local = unsigned; + double local2 = signed; + } + + void methodInteger(@Unsigned Integer unsignedInt, @Signed Integer signedInt) { + Integer local = unsignedInt; + Integer local2 = signedInt; + } + + // :: error: (type.invalid.annotations.on.use) + void methodDoubleWrapper(@Unsigned Double unsigned, @Signed Double signed) { + // :: error: (assignment.type.incompatible) + Double local = unsigned; + Double local2 = signed; + } +} diff --git a/checker/tests/signedness/PrimitiveCasts.java b/checker/tests/signedness/PrimitiveCasts.java new file mode 100644 index 00000000000..895b04e19a4 --- /dev/null +++ b/checker/tests/signedness/PrimitiveCasts.java @@ -0,0 +1,32 @@ +import org.checkerframework.checker.signedness.qual.Unsigned; + +public class PrimitiveCasts { + + void shortToChar1(short s) { + // :: warning: (cast.unsafe) + char c = (char) s; + } + + // These are Java errors. + // void shortToChar2(short s) { + // char c = s; + // } + // char shortToChar3(short s) { + // return s; + // } + + void intToDouble1(@Unsigned int ui) { + // :: warning: (cast.unsafe) + double d = (double) ui; + } + + void intToDouble2(@Unsigned int ui) { + // :: error: (assignment.type.incompatible) + double d = ui; + } + + double intToDouble3(@Unsigned int ui) { + // :: error: (return.type.incompatible) + return ui; + } +} diff --git a/checker/tests/signedness/SignednessAssignments.java b/checker/tests/signedness/SignednessAssignments.java new file mode 100644 index 00000000000..668893710d9 --- /dev/null +++ b/checker/tests/signedness/SignednessAssignments.java @@ -0,0 +1,121 @@ +import org.checkerframework.checker.signedness.qual.Signed; +import org.checkerframework.checker.signedness.qual.SignedPositive; +import org.checkerframework.checker.signedness.qual.Unsigned; + +public class SignednessAssignments { + + @Signed byte sb; + @Unsigned byte ub; + @Signed Byte sB; + @Unsigned Byte uB; + + @Unsigned char uc; + @Unsigned Character uC; + + @Signed short ss; + @Unsigned short us; + @Signed Short sS; + @Unsigned Short uS; + + @Signed int si; + @Unsigned int ui; + @Signed Integer sI; + @Unsigned Integer uI; + + @Signed long sl; + @Unsigned long ul; + @Signed Long sL; + @Unsigned Long uL; + + void assignmentsByte() { + @Signed byte i1 = sb; + @Unsigned byte i2 = ub; + @Signed byte i3 = sB; + @Unsigned byte i4 = uB; + + @Signed Byte i91 = sb; + @Unsigned Byte i92 = ub; + @Signed Byte i93 = sB; + @Unsigned Byte i94 = uB; + } + + void assignmentsShort() { + @SignedPositive short i1 = sb; + @SignedPositive short i2 = ub; + @SignedPositive short i3 = sB; + @SignedPositive short i4 = uB; + + @Signed short i9 = ss; + @Unsigned short i10 = us; + @Signed short i11 = sS; + @Unsigned short i12 = uS; + + @Signed Short i91 = ss; + @Unsigned Short i92 = us; + @Signed Short i93 = sS; + @Unsigned Short i94 = uS; + } + + void assignmentsChar() { + // These are commented out because they are Java errors. + // @Unsigned char i2 = ub; + // @Unsigned char i4 = uB; + // @Unsigned char i10 = us; + // @Unsigned char i12 = uS; + } + + void assignmentsInt() { + @SignedPositive int i1 = sb; + @SignedPositive int i2 = ub; + @SignedPositive int i3 = sB; + @SignedPositive int i4 = uB; + + @SignedPositive int i6 = uc; + @SignedPositive int i8 = uC; + + @SignedPositive int i9 = ss; + @SignedPositive int i10 = us; + @SignedPositive int i11 = sS; + @SignedPositive int i12 = uS; + + @Signed int i13 = si; + @Unsigned int i14 = ui; + @Signed int i15 = sI; + @Unsigned int i16 = uI; + + @Signed Integer i91 = si; + @Unsigned Integer i92 = ui; + @Signed Integer i93 = sI; + @Unsigned Integer i94 = uI; + } + + void assignmentsLong() { + @SignedPositive long i1 = sb; + @SignedPositive long i2 = ub; + @SignedPositive long i3 = sB; + @SignedPositive long i4 = uB; + + @SignedPositive long i6 = uc; + @SignedPositive long i8 = uC; + + @SignedPositive long i9 = ss; + @SignedPositive long i10 = us; + @SignedPositive long i11 = sS; + @SignedPositive long i12 = uS; + + @SignedPositive long i13 = si; + @SignedPositive long i14 = ui; + @SignedPositive long i15 = sI; + @SignedPositive long i16 = uI; + + @Signed long i17 = sl; + @Unsigned long i18 = ul; + @Signed long i19 = sL; + @Unsigned long i20 = uL; + + @Signed Long i91 = sl; + @Unsigned Long i92 = ul; + @Signed Long i93 = sL; + @Unsigned Long i94 = uL; + } +} diff --git a/checker/tests/signedness/ValueIntegration.java b/checker/tests/signedness/ValueIntegration.java index 83a194ba8f8..ae4ecc860d7 100644 --- a/checker/tests/signedness/ValueIntegration.java +++ b/checker/tests/signedness/ValueIntegration.java @@ -53,6 +53,8 @@ public void ByteValRules( ptest = bmixed; } + // Character and char are always @Unsigned, never @Signed. + /* public void CharValRules( @IntVal({0, 127}) char c, @IntVal({128, 255}) char upure, @@ -69,35 +71,36 @@ public void CharValRules( ptest = c; stest = upure; - // :: error: (assignment.type.incompatible) + // XX error: (assignment.type.incompatible) gtest = upure; - // :: error: (assignment.type.incompatible) + // XX error: (assignment.type.incompatible) ptest = upure; stest = umixed; - // :: error: (assignment.type.incompatible) + // XX error: (assignment.type.incompatible) gtest = umixed; - // :: error: (assignment.type.incompatible) + // XX error: (assignment.type.incompatible) ptest = umixed; stest = spure; - // :: error: (assignment.type.incompatible) + // XX error: (assignment.type.incompatible) gtest = spure; - // :: error: (assignment.type.incompatible) + // XX error: (assignment.type.incompatible) ptest = spure; stest = smixed; - // :: error: (assignment.type.incompatible) + // XX error: (assignment.type.incompatible) gtest = smixed; - // :: error: (assignment.type.incompatible) + // XX error: (assignment.type.incompatible) ptest = smixed; stest = bmixed; - // :: error: (assignment.type.incompatible) + // XX error: (assignment.type.incompatible) gtest = bmixed; - // :: error: (assignment.type.incompatible) + // XX error: (assignment.type.incompatible) ptest = bmixed; } + */ public void ShortValRules( @IntVal({0, 32767}) short c, @@ -279,6 +282,8 @@ public void ByteRangeRules( ptest = bmixed; } + // Character and char are always @Unsigned, never @Signed. + /* public void CharRangeRules( @IntRange(from = 0, to = 127) char c, @NonNegative char nnc, @@ -305,35 +310,36 @@ public void CharRangeRules( ptest = pc; stest = upure; - // :: error: (assignment.type.incompatible) + // XX error: (assignment.type.incompatible) gtest = upure; - // :: error: (assignment.type.incompatible) + // XX error: (assignment.type.incompatible) ptest = upure; stest = umixed; - // :: error: (assignment.type.incompatible) + // XX error: (assignment.type.incompatible) gtest = umixed; - // :: error: (assignment.type.incompatible) + // XX error: (assignment.type.incompatible) ptest = umixed; stest = spure; - // :: error: (assignment.type.incompatible) + // XX error: (assignment.type.incompatible) gtest = spure; - // :: error: (assignment.type.incompatible) + // XX error: (assignment.type.incompatible) ptest = spure; stest = smixed; - // :: error: (assignment.type.incompatible) + // XX error: (assignment.type.incompatible) gtest = smixed; - // :: error: (assignment.type.incompatible) + // XX error: (assignment.type.incompatible) ptest = smixed; stest = bmixed; - // :: error: (assignment.type.incompatible) + // XX error: (assignment.type.incompatible) gtest = bmixed; - // :: error: (assignment.type.incompatible) + // XX error: (assignment.type.incompatible) ptest = bmixed; } + */ public void ShortRangeRules( @IntRange(from = 0, to = 32767) short c, diff --git a/checker/tests/tainting/HasQualParamDefaults.java b/checker/tests/tainting/HasQualParamDefaults.java index e568139cac5..fa2cb03983f 100644 --- a/checker/tests/tainting/HasQualParamDefaults.java +++ b/checker/tests/tainting/HasQualParamDefaults.java @@ -44,6 +44,31 @@ public Buffer append(@PolyTainted String s) { someString = s; return s; } + + void initializeLocalTainted(@Tainted Buffer b) { + Buffer local = b; + @Tainted Buffer copy1 = local; + // :: error: (assignment.type.incompatible) + @Untainted Buffer copy2 = local; + } + + void initializeLocalUntainted(@Untainted Buffer b) { + Buffer local = b; + @Untainted Buffer copy1 = local; + // :: error: (assignment.type.incompatible) + @Tainted Buffer copy2 = local; + } + + void initializeLocalPolyTainted(@PolyTainted Buffer b) { + Buffer local = b; + @PolyTainted Buffer copy = local; + } + + void noInitializer(@Untainted Buffer b) { + Buffer local; + // :: error: (assignment.type.incompatible) + local = b; + } } class Use { @@ -83,4 +108,49 @@ void creation() { @PolyTainted Buffer b3 = new @PolyTainted Buffer(); } } + + // For classes with @HasQualifierParameter, different defaulting rules are applied on that type + // inside the class body and outside the class body, so local variables need to be tested + // outside the class as well. + class LocalVars { + void initializeLocalTainted(@Tainted Buffer b) { + Buffer local = b; + @Tainted Buffer copy1 = local; + // :: error: (assignment.type.incompatible) + @Untainted Buffer copy2 = local; + } + + void initializeLocalUntainted(@Untainted Buffer b) { + Buffer local = b; + @Untainted Buffer copy1 = local; + // :: error: (assignment.type.incompatible) + @Tainted Buffer copy2 = local; + } + + void initializeLocalPolyTainted(@PolyTainted Buffer b) { + Buffer local = b; + @PolyTainted Buffer copy = local; + } + + void noInitializer(@Untainted Buffer b) { + Buffer local; + // :: error: (assignment.type.incompatible) + local = b; + } + + // These next two cases test circular dependencies. Calculating the type of a local variable + // looks at the type of initializer, but if the type of the initializer depends on the type + // of the variable, then infinite recursion could occur. + + void testTypeVariableInference() { + GenericWithQualParam set = new GenericWithQualParam<>(); + } + + void testVariableInOwnInitializer() { + Buffer b = (b = null); + } + } + + @HasQualifierParameter(Tainted.class) + static class GenericWithQualParam {} } diff --git a/checker/tests/tainting/InitializerDataflow.java b/checker/tests/tainting/InitializerDataflow.java new file mode 100644 index 00000000000..73d1f484e98 --- /dev/null +++ b/checker/tests/tainting/InitializerDataflow.java @@ -0,0 +1,23 @@ +import org.checkerframework.checker.tainting.qual.PolyTainted; +import org.checkerframework.checker.tainting.qual.Tainted; +import org.checkerframework.checker.tainting.qual.Untainted; +import org.checkerframework.framework.qual.HasQualifierParameter; + +public class InitializerDataflow { + @HasQualifierParameter(Tainted.class) + static class Buffer {} + + @PolyTainted Buffer id(@PolyTainted String s) { + return null; + } + + void methodBuffer(@Untainted String s) { + Buffer b1 = id(s); + + String local = s; + Buffer b2 = id(local); + + @Untainted String local2 = s; + Buffer b3 = id(local2); + } +} diff --git a/dataflow/build.gradle b/dataflow/build.gradle index b8d6545b63c..4d54727f3fa 100644 --- a/dataflow/build.gradle +++ b/dataflow/build.gradle @@ -48,7 +48,7 @@ task liveVariableTest(dependsOn: compileTestJava, group: 'Verification') { if (!JavaVersion.current().java9Compatible) { jvmArgs += "-Xbootclasspath/p:${configurations.javacJar.asPath}" } - classpath = sourceSets.test.compileClasspath + classpath = sourceSets.test.runtimeClasspath classpath += sourceSets.test.output main = 'livevar.LiveVariable' } @@ -59,3 +59,22 @@ task liveVariableTest(dependsOn: compileTestJava, group: 'Verification') { } } } + +task issue3447Test(dependsOn: compileTestJava, group: 'Verification') { + description 'Test issue 3447 test case for backward analysis.' + inputs.file('tests/issue3447/Test.java') + delete('tests/issue3447/Out.txt') + delete('tests/issue3447/Test.class') + doLast { + javaexec { + workingDir = 'tests/issue3447' + if (!JavaVersion.current().java9Compatible) { + jvmArgs += "-Xbootclasspath/p:${configurations.javacJar.asPath}" + } + classpath = sourceSets.test.runtimeClasspath + classpath += sourceSets.test.output + + main = 'livevar.LiveVariable' + } + } +} diff --git a/dataflow/manual/Makefile b/dataflow/manual/Makefile index 5177f8006dc..149fe75b639 100644 --- a/dataflow/manual/Makefile +++ b/dataflow/manual/Makefile @@ -1,4 +1,4 @@ -dataflow.pdf: dataflow.tex content.tex +dataflow.pdf: dataflow.tex content.tex examples pdflatex dataflow pdflatex dataflow diff --git a/dataflow/manual/content.tex b/dataflow/manual/content.tex index 68fbb53b732..e743f9939b4 100644 --- a/dataflow/manual/content.tex +++ b/dataflow/manual/content.tex @@ -8,33 +8,17 @@ \section{Introduction} \href{https://github.com/uber/NullAway}{NullAway}, and other contexts. The primary purpose of the Dataflow Framework is to estimate values: -that is, to determine that, on a particular line of source code, what -values a variable might contain. This can also determine that a variable -has a more precise type than its declared type. This enables -flow-sensitive type checking in the Checker Framework, which -reduces the burden of annotating a program. The Dataflow Framework was -designed with several goals in mind. First, to encourage other uses -of the framework, it is written as a separate package that can be -built and used with no dependence on the Checker Framework. Second, -the framework is currently intended to support analysis but not -transformation, so it provides information that can be used by a type -checker or an IDE, but it does not support optimization. Third, the -framework aims to minimize the burden on developers who build on top -of it. In particular, the hierarchy of analysis classes is designed -to reduce the effort required to implement a new flow-sensitive type -checker in the Checker Framework. The -\href{https://docs.google.com/document/d/1oYzbOrrS4ZEEx4wQgIHbijNzcI5CiQAq_-1NrOS8JME/edit?usp=sharing}{Dataflow User's Guide} -gives an introduction to customizing dataflow to add checker specific -enhancements. +for each line of source code, it determines what +values each variable might contain. The Dataflow Framework's result (\autoref{sec:analysis_result_class}) is an abstract value for each expression (an estimate of the expression's run-time value) and a store at each program point. A -store maps variables and other distinguished expressions to abstract -values. As a pre-pass, the Dataflow Framework transforming an input +store maps variables and other expressions to abstract +values. As a pre-pass, the Dataflow Framework transforms an input AST into a control flow graph (\autoref{sec:cfg}) consisting of basic -blocks made up of nodes representing single operations. To produce -its output, the Checker Framework performs iterative data flow +blocks made up of nodes representing single operations. An analysis +performs iterative data flow analysis over the control flow graph. The effect of a single node on the dataflow store is represented by a transfer function, which takes an input store and a node and produces an output store. Once the @@ -42,10 +26,25 @@ \section{Introduction} code. In the Checker Framework, the abstract values to be computed are -annotated types. An individual checkers can customize its analysis by +annotated types. An individual checker can customize its analysis by extending the abstract value class and by overriding the behavior of the transfer function for particular node types. +The Dataflow Framework was +designed with several goals in mind. First, to encourage use +beyond the Checker Framework, it is written as a separate package that can be +built and used with no dependence on the Checker Framework. Second, +the framework supports analysis but not +transformation, so it provides information that can be used by a type +checker or an IDE, but it does not support optimization. Third, the +framework aims to minimize the burden on developers who build on top +of it. In particular, the hierarchy of analysis classes is designed +to reduce the effort required to implement a new flow-sensitive type +checker in the Checker Framework. The +\href{https://docs.google.com/document/d/1oYzbOrrS4ZEEx4wQgIHbijNzcI5CiQAq_-1NrOS8JME/edit?usp=sharing}{Dataflow User's Guide} +gives an introduction to customizing dataflow to add checker-specific +enhancements. + \begin{workinprogress} Paragraphs colored in gray with a gray bar on the left side (just like this one) contain questions, additional comments or indicate @@ -62,9 +61,10 @@ \subsection{Projects} % TODO: Update The source code of the combined Checker Framework and Dataflow -Framework is divided into five projects: \code{javacutil}, -\code{dataflow}, \code{stubparser}, \code{framework}, and \code{checker}, -which can be built into distinct jar files. +Framework is divided into multiple projects: \code{javacutil}, +\code{dataflow}, \code{framework}, and \code{checker}, +which can be built into distinct jar files. \code{checker.jar} is a fat +jar that contains all of these, plus the Stub Parser. \code{javacutil} provides convenient interfaces to routines in Oracle's javac library. There are utility classes for interacting @@ -84,18 +84,13 @@ \subsection{Projects} control flow graphs and the base classes required for flow analysis. These classes are described in detail in \autoref{sec:node_classes}. -\code{stubparser} contains the stub-file parsing project. - \code{framework} contains the framework aspects of the Checker Framework, including the derived classes for flow analysis of annotated types which are described later in this document. \code{checker} contains the type system-specific checkers. -The \code{dataflow} project depends on \code{javacutil}, the -\code{framework} project depends on both \code{dataflow} and -\code{javacutil}, \code{stubparser} has no dependencies, and -\code{checker} depends on \code{framework} and \code{stubparser}. +The \code{dataflow} project depends only on \code{javacutil}. \subsection{Classes} @@ -111,12 +106,14 @@ \subsection{Classes} \subsubsection{Nodes} \label{sec:node_classes} -Dataflow doesn't actually work on trees; it works on Nodes. Nodes -simplify writing a dataflow analysis by separating the dataflow -analysis from the original source code. +Dataflow doesn't actually work on trees; it works on Nodes. A Node class represents an individual operation of a program, including arithmetic operations, logical operations, method calls, -variable references, array accesses, etc. \autoref{tab:nodes} lists +variable references, array accesses, etc. +Nodes +simplify writing a dataflow analysis by separating the dataflow +analysis from the original source code. +\autoref{tab:nodes} on page~\pageref{tab:nodes} lists the Node types. \begin{verbatim} @@ -130,9 +127,8 @@ \subsubsection{Nodes} \subsubsection{Blocks} \label{sec:block_classes} -Nodes are grouped into basic blocks using a hierarchy of Block -classes. The hierarchy is composed of five interfaces, two abstract -classes, and four concrete classes. +The Block +classes represent basic blocks. \begin{verbatim} package org.checkerframework.dataflow.cfg.block; @@ -415,7 +411,7 @@ \subsubsubsection{TransferFunction} \end{verbatim} The Regex Checker's transfer function overrides visitMethodInvocation -to special case isRegex and asRegex methods. +to special-case the \code{isRegex} and \code{asRegex} methods. \begin{verbatim} package org.checkerframework.checker.regex; @@ -533,37 +529,31 @@ \subsubsection{AnnotatedTypeFactory} \end{verbatim} -\begin{workinprogress} -We should investigate whether we can optimize CFG generation for -aggregate and compound checkers. -\end{workinprogress} - - \section{The Control-Flow Graph} \label{sec:cfg} -%In this section, we describe the control-flow graph (CFG), and the -%translation from the Java abstract syntax tree (AST) to the CFG. - -This section describes the control-flow graph (CFG), which is used to -represent a single method or field initialization, and the translation -from the abstract syntax tree (AST) to the CFG\@. (The Dataflow -Framework described here is designed to perform an intra-procedural +A control-flow graph (CFG) represents a single method or field +initialization. (The Dataflow Framework performs an intra-procedural analysis. This analysis is modular and every method is considered in -isolation.) We start with a simple example, then give a more formal +isolation.) +This section also describes the translation from the abstract syntax tree +(AST) to the CFG\@. +We start with a simple example, then give a more formal definition of the CFG and its properties, and finally describe the translation from the AST to the CFG. -As is standard, a control-flow graph in the framework is a set of +As is standard, a control-flow graph is a set of basic blocks that are linked by control-flow edges. Possibly less standard, every basic block consists of a sequence of so-called nodes, -which correspond to a minimal Java operation or expression. +each of which represents a minimal Java operation or expression. -\flow{Simple}{A simple Java code snippet to introduce the CFG.} +\flow{CFGSimple}{.33}{1.1}{A simple Java code snippet to introduce the CFG. +In CFG visualizations, special basic blocks are shown as ovals; +conditional basic blocks are polygons with eight sides; and regular and exception +basic blocks are rectangles.} -Consider the method \code{test} of \autoref{lst:CFGSimple} whose -control-flow graph is shown in \autoref{fig:CFGSimple}. The if +Consider the method \code{test} of \autoref{fig:CFGSimple}. The if conditional got translated to a \emph{conditional basic block} (octagon) with two successors. There are also two special basic blocks (ovals) to denote the entry and exit point of the method. @@ -610,29 +600,24 @@ \subsection{Formal Definition of the Control-Flow Graph} point of the method and thus is the only basic block without predecessors. \item Exit block. This basic block denotes the (normal) - exit of a method, and it does not have successors. + exit of a method, and it has no successors. \item Exceptional exit block, which indicates exceptional termination of the method. As an exit block, this block - does not have successors. + has no successors. \end{itemize} Every method has exactly one entry block, zero or one exit blocks, - and zero or one exceptional exit blocks. However, there is - either an exit block or an exceptional exit block. - \begin{workinprogress} - Is this an exclusive or? Or can a method have both a regular - exit block and an exceptional exit block? I think the latter - is true. - \end{workinprogress} + and zero or one exceptional exit blocks. There is always + either an exit block, an exceptional exit block, or both. \item \textbf{Exception basic block.} An \emph{exception basic block} contains exactly one node that \emph{might} throw an exception at runtime (e.g., a method call). There are zero - or one non-exceptional successors, and one or more - exceptional successors (see \autoref{def:edges}). But in all - cases there is at least one successor (regular or - exceptional), and only a basic block containing a + or one non-exceptional successors (only a basic block containing a \code{throw} statement does not have a non-exceptional - successor. + successor). There are one or more + exceptional successors (see \autoref{def:edges}). In all + cases there is at least one successor (regular or + exceptional). \item \textbf{Conditional basic block.} A \emph{conditional basic block} does not contain any nodes and is used as a @@ -653,23 +638,18 @@ \subsection{Formal Definition of the Control-Flow Graph} The Java implementation of the four block types above is described in \autoref{sec:block_classes}. -In the visualizations used in this document (e.g., in -\autoref{fig:CFGSimple}), special basic blocks are shown as ovals, -conditional basic blocks are polygons with eight sides, and any other -basic block appears as a rectangle. - \begin{definition}[Control-Flow Graph Edges] \label{def:edges} The basic blocks of a control-flow graph are connected by directed \emph{edges}. If $b_1$ and $b_2$ are connected by a directed edge -$(b_1,b_2)$, we call $b_1$ the predecessor of $b_2$, and we call $b_2$ -the successor of $b_1$. In a control-flow graph, there are three +$(b_1,b_2)$, we call $b_1$ a predecessor of $b_2$, and we call $b_2$ +a successor of $b_1$. In a control-flow graph, there are three types of edges: \begin{enumerate} \item \textbf{Exceptional edges}. An \emph{exceptional edge} connects an exception basic block with its exceptional successors, and it is labeled by the most general exception that - might cause execution to take this edge during runtime. Note + might cause execution to take this edge during run time. Note that the outgoing exceptional edges of a basic block do not need to have mutually exclusive labels; the semantics is that the control flow follows the most specific edge. For instance, if @@ -707,7 +687,7 @@ \subsection{Formal Definition of the Control-Flow Graph} A \emph{node} is a minimal Java operation or expression. It is minimal in the sense that it cannot be decomposed further into subparts between which control flow occurs. Examples for such - nodes include integer literals, an addition node (that performs + nodes include integer literals, an addition node (which performs the mathematical addition of two nodes) or a method call. Control flow such as \code{if} and \code{break} are not represented as nodes. The full list of nodes is given in \autoref{tab:nodes} and @@ -865,7 +845,7 @@ \subsection{Formal Definition of the Control-Flow Graph} \label{tab:nodes} \end{longtable} -\autoref{tab:nodesWithException} shows all node types which can possibly throw +\autoref{tab:nodesWithException} shows all node types that can possibly throw an exception and the exception type to be thrown. Java class name of nodes are simplified as with \autoref{tab:nodes}. All exception types in \autoref{tab:nodesWithException} are in package \code{java.lang}. @@ -876,14 +856,10 @@ \subsection{Formal Definition of the Control-Flow Graph} it's not recommended to catch it due to a critical situation which is not to able to execute JVM and handle in an application. - \begin{longtable}{lp{0.6\linewidth}l} - \midrule - \multicolumn{2}{c}{\autoref{tab:nodesWithException}: All node types could throw Exception and types to be thrown.} \\ \\ - \textbf{Node type} & \textbf{Exception type} \\ \midrule \endfirsthead - - \textbf{Node type} & \textbf{Exception type} \\ \midrule \endhead - \hline \multicolumn{2}{|c|}{{Continued on next page}} \\ \hline \endfoot - \endlastfoot +\begin{table} + \begin{tabular}{ll} + \hline + \textbf{Node type} & \textbf{Exception type} \\ \hline \code{ArrayAccess} & \code{NullPointerException}, \code{ArrayIndexOutOfBoundsException} \\ \code{FieldAccess} & \code{NullPointerException} \\ @@ -895,11 +871,12 @@ \subsection{Formal Definition of the Control-Flow Graph} \code{TypeCast} & \code{ClassCastException} \\ \code{Throw} & Type of \code{e} when \code{throw e} \\ \code{AssertionError} & \code{AssertionError} \\ - \midrule + \hline + \end{tabular} - \caption{All node types could throw Exception and types to be thrown.} + \caption{All node types that could throw Exception, and the types to be thrown.} \label{tab:nodesWithException} - \end{longtable} +\end{table} \begin{workinprogress} ThisLiteral shouldn't be considered a literal value node, because a use of @@ -951,15 +928,15 @@ \subsubsection{Program Structure} \label{sec:prog-structure} Java programs are structured using high-level programming constructs -such as different variants of loops, if-then-else constructs, +such as loops, if-then-else constructs, try-catch-finally blocks or switch statements. During the translation from the AST to the CFG some of this program structure is lost and all non-sequential control flow is represented by two low-level constructs: conditional basic blocks and control-flow edges between basic blocks. For instance, a \code{while} loop is translated into its condition followed by a conditional basic block that models the two -possible outcomes of the condition: either, the control flow follows -the `true' branch and continues with the loops body, or goes to the +possible outcomes of the condition: either the control flow follows +the `true' branch and continues with the loop's body, or control goes to the `false' successor and executes the first statement after the loop. @@ -970,35 +947,35 @@ \subsubsection{Assignment} be evaluated even if the left-hand side of the assignment causes an exception. This semantics is faithfully represented in the CFG produced by the translation. An example of a field assignment -exhibiting this behavior is shown in \autoref{lst:CFGFieldAssignment}. +exhibiting this behavior is shown in \autoref{fig:CFGFieldAssignment}. -\flow{FieldAssignment}{Control flow for a field assignment is not strictly +\flow{CFGFieldAssignment}{.33}{1}{Control flow for a field assignment is not strictly left-to-right (cf.\ \jlsref{15.26.1}), which is properly handled by the translation.} \subsubsection{Postfix/Prefix Increment/Decrement} \label{sec:postpre-incdec} -Both of postfix and prefix increment or decrement have a side effect to -update the variable or field. To represent this side effect, Dataflow -Framework create an artificial assignment node like \code{n = n + 1} +Postfix and prefix increment and decrement have a side effect to +update the variable or field. To represent this side effect, the Dataflow +Framework creates an artificial assignment node like \code{n = n + 1} for \code{++n} or \code{n++}. This artificial assignment node is stored in \code{unaryAssignNodeLookup} of \code{ControlFlowGraph}. The assignment node is also stored in \code{treeLookup} for prefix increment or decrement so that the result of it is after the assignment. However, the node before the assignment is stored in \code{treeLookup} for postfix increment or decrement because the result of it should be before the assignment. For further information -about node-tree mapping, see \autoref{sec:conversions} also. +about node-tree mapping, see \autoref{sec:conversions}. \subsubsection{Conditional stores} \label{sec:cond-stores} The Dataflow Framework extracts information from control-flow splits -that occur in if, for, while, and switch statements. In order to have +that occur in \code{if}, \code{for}, \code{while}, and \code{switch} statements. In order to have the information available at the split, we eagerly produce two stores contained in a \code{ConditionalTransferResult} after certain boolean-valued expressions. The stores are called the \emph{then} and \emph{else} stores. So, for example, after the expression \code{x == - null}, two different stores will be created. The Nullness Checkers + null}, two different stores will be created. The Nullness Checker would produce a then store that maps \code{x} to @Nullable and an else store that maps \code{x} to @NonNull. @@ -1023,7 +1000,7 @@ \subsubsection{Branches} successor. Consider the control flow graph generated for the simple if statement -in \autoref{lst:CFGIfStatement}. The conditional expression \code{b1} +in \autoref{fig:CFGIfStatement}. The conditional expression \code{b1} immediately precedes the \code{ConditionalBlock}, represented by the octagonal node. The \code{ConditionalBlock} is followed by both a then and an else successor block, after which control flow merges back @@ -1036,7 +1013,7 @@ \subsubsection{Branches} More precise rules are used to preserve dataflow information for short-circuiting expressions, as described in \autoref{sec:cond-exp}. -\flow{IfStatement}{Example of an if statement translated into a +\flow{CFGIfStatement}{.33}{1.25}{Example of an if statement translated into a \code{ConditionalBlock}.} \subsubsection{Conditional Expressions} @@ -1051,7 +1028,7 @@ \subsubsection{Conditional Expressions} additional dataflow information. An example program using conditional or is shown in -\autoref{lst:CFGConditionalOr}. Note that the CFG correctly +\autoref{fig:CFGConditionalOr}. Note that the CFG correctly represents short-circuiting. The expression \code{b2 || b3} is only executed if \code{b1} is false and \code{b3} is only evaluated if \code{b1} and \code{b2} are false. @@ -1075,7 +1052,7 @@ \subsubsection{Conditional Expressions} along those edges need to be kept in the else store of the block containing \code{b1 || (b2 || b3)}. -\flow{ConditionalOr}{Example of a conditional or expression +\flow{CFGConditionalOr}{.33}{1.33}{Example of a conditional or expression (\code{||}) with short-circuiting and more precise flow rules.} @@ -1086,12 +1063,7 @@ \subsubsection{Implicit \code{this} access} left out). To relieve the user of the Dataflow Framework from manually determining the two cases, we consistently use \code{FieldAccessNode} for field accesses, where the receiver might be -an \code{ImplicitThisNode}. For instance, this is shown in the -earlier example \autoref{lst:CFGFieldAssignment}. - -\begin{workinprogress} -I don't see a \code{implicitThisNode} in \autoref{lst:CFGFieldAssignment}. -\end{workinprogress} +an \code{ImplicitThisNode}. \subsubsection{Assert statements} @@ -1105,20 +1077,18 @@ \subsubsection{Assert statements} annotations. However, when assertions are disabled, it would be unsound to assume that they had any effect on dataflow information. -Our solution is to offer the user of the Dataflow Framework, and -ultimately the user of the Checker Framework, the option of stating that +The user of the Dataflow Framework may specify that assertions are enabled or disabled. When assertions are assumed to be -disabled, no CFG Nodes are built for the assert statement at all. +disabled, no CFG Nodes are built for the assert statement. When assertions are assumed to be enabled, CFG Nodes are built to represent the condition of the assert statement and, in the else successor of a ConditionalBlock, CFG Nodes are built to represent the detail expression of the assert, if any. -If assertions are not assumed to be enabled or disabled, then we -generate a CFG that is conservative and represents the fact that the +If assertions are not assumed to be enabled or disabled, then +the CFG is conservative and represents the fact that the assert statement may execute or may not. This takes the form of a -ConditionalBlock that branches on a fake variable. For example, the -code in \autoref{lst:CFGAssert} produces the control flow graph in +ConditionalBlock that branches on a fake variable. For example, see \autoref{fig:CFGAssert}. The fake variable named \code{assertionsEnabled#num0} controls the first ConditionalBlock. The then successor of the ConditionalBlock is the same subgraph of CFG @@ -1127,19 +1097,10 @@ \subsubsection{Assert statements} subgraph of CFG Nodes that would be created if assertions were assumed to be disabled. -\flow{Assert}{Example of an assert statement translated with +\flow{CFGAssert}{.15}{2.9}{Example of an assert statement translated with assertions neither assumed to be enabled nor assumed to be disabled.} -\begin{workinprogress} -How should the user choose between the three possibilities? -\end{workinprogress} - -\begin{workinprogress} -Why are there two basic blocks for the \code{AssertionError} and then -the \code{throw}? Why are they not in the same basic block, as there -is no alternate control flow? -\end{workinprogress} \subsubsection{Varargs method invocation} \label{sec:varargs} @@ -1148,28 +1109,28 @@ \subsubsection{Varargs method invocation} \code{m(1, 2, 3)} will be compiled as \code{m(new int[]\{1, 2, 3\})} when the signature of \code{m} is \code{m(int... args)}. Dataflow Framework creates an \code{ArrayCreationNode} with initializer for varargs -in the same way as Java compiler. Note that it doesn't create an \code{ArrayCreationNode} -when the varargs is an array which is same depth with the type of -the formal parameter or \code{null} is given as actual varargs. +in the same way as the Java compiler does. +Note that it doesn't create an \code{ArrayCreationNode} +when the varargs is an array with the same depth as the type of +the formal parameter, or if \code{null} is given as the actual varargs argument. \subsubsection{Default case and fall through for switch statement} \label{sec:default-switch} -Switch statement is handled as a chain of \code{CaseNode} and nodes in +A switch statement is handled as a chain of \code{CaseNode} and nodes in the case. \code{CaseNode} makes a branch by comparing the equality of the expression of the switch statement and the expression of the case. Note that the expression of a switch statement must be executed just only -once at first of switch statement. To refer its value, a fake variable +once at the beginning of the switch statement. To refer to its value, a fake variable is created and it is assigned to a fake variable. \code{THEN_TO_BOTH} edge goes to nodes in the case and \code{ELSE_TO_BOTH} edge goes to next \code{CaseNode}. When a next is default case, it goes to nodes in the default case. If a break statement is in nodes, it creates an edge to next node of the switch statement. If there is any possibility of fall-through, an edge to the first of nodes in the next case is created after nodes in the case. -For example, the code in \autoref{lst:CFGSwitch} produces the control flow -graph in \autoref{fig:CFGSwitch}. The fake variable named \code{switch#num0} +For example, see \autoref{fig:CFGSwitch}. The fake variable named \code{switch#num0} is created and each of case nodes creates the branches. -\flow{Switch}{Example of a switch statement with case, default and fall through.} +\flow{CFGSwitch}{.21}{1.45}{Example of a switch statement with case, default and fall through.} \subsubsection{Handling \code{finally} blocks} \label{sec:try-finally} @@ -1196,12 +1157,7 @@ \subsection{AST to CFG Translation} \begin{itemize} \item \textbf{Simple extended node.} An extended node can just be a wrapper for a node that does not throw an exception, - as defined in Definition~\ref{def:node}. - \begin{workinprogress} - Say how non-exception-throwing nodes are distinguished in - Table~\autoref{tab:nodes}. Exception-throwing ones need to - include throw, call, field access, typecast, division, ... - \end{workinprogress} + as defined in Definition~\ref{def:node}. \item \textbf{Exception extended node.} Similar to a simple node, an exception extended node contains a node, but this node might throw an exception at runtime. @@ -1417,11 +1373,7 @@ \subsubsection{Conversions and node-tree mapping} \section{Dataflow Analysis} This section describes how the dataflow analysis over the control-flow -graph is performed and what the user of the framework has to implement -to define a particular analysis. - - -\subsection{Overview} +graph is performed and how to implement a particular analysis. Roughly, a dataflow analysis in the framework works as follows. Given the abstract syntax tree of a method, the framework computes the @@ -1432,49 +1384,17 @@ \subsection{Overview} functions are specific to the particular analysis and are used to approximate the runtime behavior of different statements and expressions. -An analysis result contains two parts: - -\begin{enumerate} -\item - A node-value mapping (\code{Analysis.nodeValues}) from node to - abstract value. Only nodes that can take on an abstract value are - used as keys. For example, in the Checker Framework, the mapping is - from expression nodes to annotated types. - -\item - A set of \emph{stores}. Each store maps a flow expression to an - abstract value. Each store is associated with a specific program - point. The framework keeps explicit stores for the start of each - basic block (\code{Analysis.stores}) and computes the store for - other program points on the fly. -\end{enumerate} - -\begin{workinprogress} -There needs to be a definition of ``program point''. -\end{workinprogress} - - -After an analysis has iterated to a fix-point, the computed dataflow -information is maintained in an AnalysisResult, which can map either -nodes or trees to abstract values. - - - \subsection{Managing Intermediate Results of the Analysis} \label{sec:node-mapping} \label{sec:store-management} -\begin{workinprogress} -This feels repetitive with the previous one. Combine them in whole - or in part? -\end{workinprogress} Conceptually, the dataflow analysis computes an abstract value for every node and flow expression\footnote{Certain dataflow analysis might choose not to produce an abstract value for every node. For instance, a constant propagation analysis would only be concerned - with nodes of a numerical type, and ignore other nodes.}. The + with nodes of a numerical type, and could ignore other nodes.}. The transfer function (\autoref{sec:transfer-fnc}) produces these abstract values, taking as input the abstract values computed earlier for sub-expressions. For instance, in a constant propagation analysis, @@ -1483,18 +1403,25 @@ \subsection{Managing Intermediate Results of the Analysis} \code{AdditionNode} is a constant if and only if both operands are constant. -There are two parts to the analysis result. +An analysis result contains two parts: \begin{enumerate} \item -The \emph{node-value mapping} maps \code{Node}s to their abstract -values. The framework consciously does not store the abstract value +The \emph{node-value mapping} (\code{Analysis.nodeValues}) maps \code{Node}s to their abstract +values. Only nodes that can take on an abstract value are +used as keys. For example, in the Checker Framework, the mapping is +from expression nodes to annotated types. + +The framework consciously does not store the abstract value directly in the node, to remove any coupling between the control-flow graph and a particular analysis. This allows the control-flow graph to be constructed only once, and then reused for different dataflow analyses. \item +A set of \emph{stores}. Each store maps a flow expression to an +abstract value. Each store is associated with a specific program point. + The stores tracked by an analysis implement the \code{Store} interface, which defines the following operations: \begin{itemize} @@ -1519,14 +1446,18 @@ \subsection{Managing Intermediate Results of the Analysis} \end{verbatim} Every store is associated with a particular point in the control-flow -graph, and all stores are managed by the framework. It maintains a -single store for every basic block that represents the information -available at the beginning of that block. When dataflow information +graph, and all stores are managed by the framework. It saves an explicit store +for the start of each basic block. +When dataflow information is requested for a later point in a block, the analysis applies the -transfer function to compute it from the initial store. +transfer function to recompute it from the initial store. \end{enumerate} +After an analysis has iterated to a fix-point, the computed dataflow +information is maintained in an AnalysisResult, which can map either +nodes or trees to abstract values. + \subsection{Answering Questions} \label{sec:answering-questions} @@ -1549,7 +1480,7 @@ \subsection{Answering Questions} \end{itemize} \end{enumerate} -The store may first need to be computed, as the framework does not +The store may first need to be (re-)computed, as the framework does not store all intermediate stores but rather only those for key positions as described in \autoref{sec:store-management}. @@ -1573,28 +1504,25 @@ \subsection{Answering Questions} \subsection{Transfer Function} \label{sec:transfer-fnc} -A transfer function has to provide the following: -\begin{itemize} -\item A method that returns the initial store for a method, given the - list of arguments (as \code{LocalVariableNode}s) and the - \code{MethodTree} (useful if the initial store depends on the method - signature, for instance). - - \begin{workinprogress} - Why are the arguments to the method call LocalVariableNodes? Or - should this be parameters? Make clear whether this is about an - invocation of a method or a method declaration. - \end{workinprogress} +A transfer function is an object that has a transfer method for +every \code{Node} type, and also a transfer method for procedure entry. -\item A transfer method for every \code{Node} type that takes a store +\begin{itemize} +\item A transfer method for a \code{Node} type takes a store and the node, and produces an updated store. This is achieved by implementing the \code{NodeVisitor} interface for the store type \code{S}. -These transfer methods also get access to the abstract value of any -sub-node of the node \code n under consideration. This is not limited -to immediate children, but the abstract value for any node contained -in \code n can be queried. + These transfer methods also get access to the abstract value of any + sub-node of the node \code n under consideration. This is not limited + to immediate children, but the abstract value for any node contained + in \code n can be queried. + +\item A transfer method for procedure entry returns the initial store, given the + list of parameters (as \code{LocalVariableNode}s that represent the formal + parameters) and the + \code{MethodTree} (useful if the initial store depends on the procedure + signature, for instance). \end{itemize} @@ -1654,10 +1582,7 @@ \subsection{Flow Rules} else store of its successor by not propagating information to the else store which might conflict with information already there, and conversely for \code{ELSE_TO_ELSE}. - -\begin{workinprogress} -What happens to the other store in these operations? -\end{workinprogress} +See \autoref{sec:cond-exp} for more details and an example. Currently, we only use flow rules for short-circuiting edges of conditional ands and ors. The CFG builder sets the flow rule of each @@ -1669,10 +1594,6 @@ \subsection{Flow Rules} analyzed, so it is a requirement that at least one predecessor block writes the then store and at least one writes the else store. -\begin{workinprogress} -How are these flow rules attached/changed? -\end{workinprogress} - \subsection{Concurrency} @@ -1691,10 +1612,6 @@ \subsection{Concurrency} behaves monotonically, however it is not yet used to preserve dataflow information about fields under concurrent semantics. -\begin{workinprogress} -Do we have an issue filed for the last point above? -\end{workinprogress} - \section{Example: Constant Propagation} @@ -1734,12 +1651,12 @@ \section{Example: Constant Propagation} function that considers the \code{EqualToNode}, and if it is of the form \code{a == e} for a local variable \code{a} and constant \code{e}, passes the correct information to one of the branches. This -is also shown in the example of \autoref{fig:ConstSimple}. +is also shown in \autoref{fig:ConstSimple}. -\text{Example.} A small example is shown in \autoref{lst:ConstSimple} -and \autoref{fig:ConstSimple}. +\text{Example.} A small example is shown in \autoref{fig:ConstSimple}. -\constantpropagation{Simple}{Simple sequential program to illustrate constant propagation.} +\flow{ConstSimple}{.45}{1}{Simple sequential program to illustrate constant + propagation. Intermediate analysis results are shown.} \section{Example: Live Variable} @@ -1764,22 +1681,18 @@ \section{Example: Live Variable} is a backward transfer function). The transfer function visits assignments to update the information of live variables for each node in the stores. -\textbf{Example.} An example is shown in \autoref{lst:LiveSimple} and -\autoref{fig:LiveSimple}. +\textbf{Example.} An example is shown in \autoref{fig:LiveSimple}. -\livevariable{Simple}{Simple sequential program to illustrate live variable.} +\flow{LiveSimple}{.33}{1}{Simple sequential program to illustrate live variable. Intermediate analysis results are shown.} \section{Default Analysis} -\begin{workinprogress} -I feel like there is a missing cross-reference or two to this section. -\end{workinprogress} \subsection{Overview} The default flow-sensitive analysis \code{org.checkerframework.framework.flow.CFAnalysis} -works for the qualifier hierarchy of any checker defined in the +works for any checker defined in the Checker Framework. This generality is both a strength and a weakness because the default analysis can always run but the facts it can deduce are limited. The default analysis is extensible so checkers diff --git a/dataflow/manual/dataflow.tex b/dataflow/manual/dataflow.tex index eabb57691fe..26df71afaf8 100644 --- a/dataflow/manual/dataflow.tex +++ b/dataflow/manual/dataflow.tex @@ -50,6 +50,7 @@ numberstyle=\tiny, stepnumber=1, breaklines=true, + breakatwhitespace, frame=lines, showstringspaces=false, tabsize=2, @@ -87,28 +88,26 @@ \newtheorem{definition}{Definition}[section] % control-flow graph images and listings -\newcommand{\flowlst}[2]{\lstinputlisting[caption=#2,label=lst:#1,float]{examples/#1.java}} -\newcommand{\flowimg}[2]{\begin{figure} +% Arguments 2 and 3 control how much horizontal space the code takes on the page. +\newcommand{\flow}[4]{ +\begin{figure} +\centering\vspace{0pt} +\begin{minipage}[t]{#2\textwidth} +\centering\vspace{0pt} +\begin{minipage}[t]{#3\textwidth} +\lstinputlisting{examples/#1.java} +\end{minipage}% +\end{minipage}% +\begin{minipage}[t]{.6\textwidth} +\centering\vspace{0pt} \includegraphics[scale=0.6]{examples/graphs/#1.pdf} \centering -\caption{#2} +\end{minipage} +\caption{#4} \label{fig:#1} \end{figure}} -\newcommand{\flow}[2]{ - \flowlst{CFG#1}{{#2 Its CFG is depicted in \autoref{fig:CFG#1}.}} - \flowimg{CFG#1}{The control-flow graph for \autoref{lst:CFG#1}.} -} -\newcommand{\constantpropagation}[2]{ - \flowlst{Const#1}{{#2 Its CFG and intermediate analysis results are depicted in \autoref{fig:Const#1}.}} - \flowimg{Const#1}{The control-flow graph (including intermediate analysis results) for \autoref{lst:Const#1}.} -} - -\newcommand{\livevariable}[2]{ -\flowlst{Live#1}{{#2 Its CFG and intermediate analysis results are depicted in \autoref{fig:Live#1}.}} -\flowimg{Live#1}{The control-flow graph (including intermediate analysis results) for \autoref{lst:Live#1}.} -} -% references to the java specification +% references to the Java Language Specification. \newcommand{\jlsref}[1]{JLS~\textsection{}#1} diff --git a/dataflow/manual/examples/CFGAssert.java b/dataflow/manual/examples/CFGAssert.java index 5f5050357a1..1bc3eb59135 100644 --- a/dataflow/manual/examples/CFGAssert.java +++ b/dataflow/manual/examples/CFGAssert.java @@ -1,5 +1,6 @@ class Test { - void testAssert(Object a) { - assert a != null : "Argument is null"; - } + void testAssert(Object a) { + assert a != null + : "Argument is null"; + } } diff --git a/dataflow/manual/examples/CFGConditionalOr.java b/dataflow/manual/examples/CFGConditionalOr.java index 896cf44bce7..03e263a416f 100644 --- a/dataflow/manual/examples/CFGConditionalOr.java +++ b/dataflow/manual/examples/CFGConditionalOr.java @@ -1,8 +1,8 @@ class Test { - void test(boolean b1, boolean b2, boolean b3) { - int x = 0; - if (b1 || (b2 || b3)) { - x = 1; - } + void test(boolean b1, boolean b2, boolean b3) { + int x = 0; + if (b1 || (b2 || b3)) { + x = 1; } + } } diff --git a/dataflow/manual/examples/CFGConditionalOr2.java b/dataflow/manual/examples/CFGConditionalOr2.java index 93bcf908b40..dc9b2855074 100644 --- a/dataflow/manual/examples/CFGConditionalOr2.java +++ b/dataflow/manual/examples/CFGConditionalOr2.java @@ -1,6 +1,6 @@ class Test { - void test(boolean b1, boolean b2, boolean b3) { - int x = 0; - boolean b = b1 || (b2 || b3); - } + void test(boolean b1, boolean b2, boolean b3) { + int x = 0; + boolean b = b1 || (b2 || b3); + } } diff --git a/dataflow/manual/examples/CFGFieldAssignment.java b/dataflow/manual/examples/CFGFieldAssignment.java index 0bbecdddee1..898e1882072 100644 --- a/dataflow/manual/examples/CFGFieldAssignment.java +++ b/dataflow/manual/examples/CFGFieldAssignment.java @@ -1,7 +1,7 @@ class Test { - int f; + int f; - void test(Test x) { - x.f = 1; - } + void test(Test x) { + x.f = 1; + } } diff --git a/dataflow/manual/examples/CFGIfStatement.java b/dataflow/manual/examples/CFGIfStatement.java index db83f01ef58..f4ffd99be40 100644 --- a/dataflow/manual/examples/CFGIfStatement.java +++ b/dataflow/manual/examples/CFGIfStatement.java @@ -1,10 +1,10 @@ class Test { - void testIf(boolean b1) { - int x = 0; - if (b1) { - x = 1; - } else { - x = 2; - } + void testIf(boolean b1) { + int x = 0; + if (b1) { + x = 1; + } else { + x = 2; } + } } diff --git a/dataflow/manual/examples/CFGSimple.java b/dataflow/manual/examples/CFGSimple.java index 387ba72ce61..2aedb61de84 100644 --- a/dataflow/manual/examples/CFGSimple.java +++ b/dataflow/manual/examples/CFGSimple.java @@ -1,8 +1,8 @@ class Test { - void test(boolean b) { - int x = 2; - if (b) { - x = 1; - } + void test(boolean b) { + int x = 2; + if (b) { + x = 1; } + } } diff --git a/dataflow/manual/examples/CFGSwitch.java b/dataflow/manual/examples/CFGSwitch.java index 91f2d95141c..ae8eea293ce 100644 --- a/dataflow/manual/examples/CFGSwitch.java +++ b/dataflow/manual/examples/CFGSwitch.java @@ -1,14 +1,14 @@ class Test { - void test(int x) { - switch (x) { - case 1: - int a = x; - break; - case 2: - int b = x; - default: - int c = x; - break; - } + void test(int x) { + switch (x) { + case 1: + int a = x; + break; + case 2: + int b = x; + default: + int c = x; + break; } + } } diff --git a/dataflow/manual/examples/ConstSimple.java b/dataflow/manual/examples/ConstSimple.java index 144fe4cad35..150dfd20281 100644 --- a/dataflow/manual/examples/ConstSimple.java +++ b/dataflow/manual/examples/ConstSimple.java @@ -1,16 +1,16 @@ class Test { - void test(boolean b, int a) { - int x = 1; - int y = 0; - if (b) { - x = 2; - } else { - x = 2; - y = a; - } - x = 3; - if (a == 2) { - x = 4; - } + void test(boolean b, int a) { + int x = 1; + int y = 0; + if (b) { + x = 2; + } else { + x = 2; + y = a; } + x = 3; + if (a == 2) { + x = 4; + } + } } diff --git a/dataflow/manual/examples/LiveSimple.java b/dataflow/manual/examples/LiveSimple.java index 891241c18fe..641ac2eb4fc 100644 --- a/dataflow/manual/examples/LiveSimple.java +++ b/dataflow/manual/examples/LiveSimple.java @@ -1,10 +1,10 @@ public class Test { - public void test() { - int a = 1, b = 2, c = 3; - if (a > 0) { - int d = a + c; - } else { - int e = a + b; - } + public void test() { + int a = 1, b = 2, c = 3; + if (a > 0) { + int d = a + c; + } else { + int e = a + b; } + } } diff --git a/dataflow/manual/examples/graphs/LiveSimple.pdf b/dataflow/manual/examples/graphs/LiveSimple.pdf index 53f72a05274..a1e44e54efc 100644 Binary files a/dataflow/manual/examples/graphs/LiveSimple.pdf and b/dataflow/manual/examples/graphs/LiveSimple.pdf differ 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 36ee3346a60..74b1fda1087 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,8 @@ import java.util.PriorityQueue; import java.util.Set; import javax.lang.model.element.Element; +import org.checkerframework.checker.interning.qual.FindDistinct; +import org.checkerframework.checker.interning.qual.InternedDistinct; import org.checkerframework.checker.nullness.qual.EnsuresNonNull; import org.checkerframework.checker.nullness.qual.EnsuresNonNullIf; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; @@ -74,13 +76,13 @@ public abstract class AbstractAnalysis< * !isRunning ==> (currentNode == null) * */ - protected @Nullable Node currentNode; + protected @InternedDistinct @Nullable Node currentNode; /** * The tree that is currently being looked at. The transfer function can set this tree to make * sure that calls to {@code getValue} will not return information for this given tree. */ - protected @Nullable Tree currentTree; + protected @InternedDistinct @Nullable Tree currentTree; /** The current transfer input when the analysis is running. */ protected @Nullable TransferInput currentInput; @@ -100,10 +102,19 @@ public abstract class AbstractAnalysis< * * @param currentTree the tree that should be currently looked at */ - public void setCurrentTree(Tree currentTree) { + public void setCurrentTree(@FindDistinct Tree currentTree) { this.currentTree = currentTree; } + /** + * Set the node that is currently being looked at. + * + * @param currentNode the node that should be currently looked at + */ + protected void setCurrentNode(@FindDistinct @Nullable Node currentNode) { + this.currentNode = currentNode; + } + /** * Implementation of common features for {@link BackwardAnalysisImpl} and {@link * ForwardAnalysisImpl}. @@ -150,7 +161,7 @@ public Direction getDirection() { } @Override - @SuppressWarnings("contracts.precondition.override.invalid") // implementation field + @SuppressWarnings("nullness:contracts.precondition.override.invalid") // implementation field @RequiresNonNull("cfg") public AnalysisResult getResult() { if (isRunning) { @@ -213,7 +224,7 @@ public IdentityHashMap getNodeValues() { } @Override - @SuppressWarnings("contracts.precondition.override.invalid") // implementation field + @SuppressWarnings("nullness:contracts.precondition.override.invalid") // implementation field @RequiresNonNull("cfg") public @Nullable S getRegularExitStore() { SpecialBlock regularExitBlock = cfg.getRegularExitBlock(); @@ -225,7 +236,7 @@ public IdentityHashMap getNodeValues() { } @Override - @SuppressWarnings("contracts.precondition.override.invalid") // implementation field + @SuppressWarnings("nullness:contracts.precondition.override.invalid") // implementation field @RequiresNonNull("cfg") public @Nullable S getExceptionalExitStore() { SpecialBlock exceptionalExitBlock = cfg.getExceptionalExitBlock(); @@ -251,14 +262,7 @@ public IdentityHashMap getNodeValues() { return cfg.getNodesCorrespondingToTree(t); } - /** - * Return the abstract value for {@link Tree} {@code t}, or {@code null} if no information is - * available. Note that if the analysis has not finished yet, this value might not represent the - * final value for this node. - * - * @param t the given tree - * @return the abstract value for the given tree - */ + @Override public @Nullable V getValue(Tree t) { // we don't have a org.checkerframework.dataflow fact about the current node yet if (t == currentTree) { @@ -328,9 +332,10 @@ protected TransferResult callTransferFunction( return new RegularTransferResult<>(null, transferInput.getRegularStore()); } transferInput.node = node; - currentNode = node; + setCurrentNode(node); + @SuppressWarnings("nullness") // CF bug: "INFERENCE FAILED" TransferResult transferResult = node.accept(transferFunction, transferInput); - currentNode = null; + setCurrentNode(null); if (node instanceof AssignmentNode) { // store the flow-refined value effectively for final local variables AssignmentNode assignment = (AssignmentNode) node; @@ -432,7 +437,7 @@ protected static class Worklist { * forward analysis. */ public class ForwardDFOComparator implements Comparator { - @SuppressWarnings("unboxing.of.nullable") + @SuppressWarnings("nullness:unboxing.of.nullable") @Override public int compare(Block b1, Block b2) { return depthFirstOrder.get(b1) - depthFirstOrder.get(b2); @@ -444,7 +449,7 @@ public int compare(Block b1, Block b2) { * backward analysis. */ public class BackwardDFOComparator implements Comparator { - @SuppressWarnings("unboxing.of.nullable") + @SuppressWarnings("nullness:unboxing.of.nullable") @Override public int compare(Block b1, Block b2) { return depthFirstOrder.get(b2) - depthFirstOrder.get(b1); diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/analysis/Analysis.java b/dataflow/src/main/java/org/checkerframework/dataflow/analysis/Analysis.java index 476c61a75cd..13964b5235c 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/analysis/Analysis.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/analysis/Analysis.java @@ -1,5 +1,6 @@ package org.checkerframework.dataflow.analysis; +import com.sun.source.tree.Tree; import java.util.IdentityHashMap; import java.util.Map; import org.checkerframework.checker.nullness.qual.Nullable; @@ -116,6 +117,16 @@ S runAnalysisFor( */ @Nullable V getValue(Node n); + /** + * Return the abstract value for {@link Tree} {@code t}, or {@code null} if no information is + * available. Note that if the analysis has not finished yet, this value might not represent the + * final value for this node. + * + * @param t the given tree + * @return the abstract value for the given tree + */ + @Nullable V getValue(Tree t); + /** * Returns the regular exit store, or {@code null}, if there is no such store (because the * method cannot exit through the regular exit block). 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 888b0a801d7..4c665047980 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/analysis/BackwardAnalysisImpl.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/analysis/BackwardAnalysisImpl.java @@ -4,13 +4,13 @@ import java.util.List; import java.util.ListIterator; import java.util.Map; +import org.checkerframework.checker.interning.qual.FindDistinct; import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.checker.nullness.qual.RequiresNonNull; import org.checkerframework.dataflow.analysis.Store.FlowRule; import org.checkerframework.dataflow.cfg.ControlFlowGraph; import org.checkerframework.dataflow.cfg.UnderlyingAST; import org.checkerframework.dataflow.cfg.block.Block; -import org.checkerframework.dataflow.cfg.block.BlockImpl; import org.checkerframework.dataflow.cfg.block.ConditionalBlock; import org.checkerframework.dataflow.cfg.block.ExceptionBlock; import org.checkerframework.dataflow.cfg.block.RegularBlock; @@ -115,7 +115,7 @@ public void performAnalysisBlock(Block b) { firstNode = node; } // Propagate store to predecessors - for (BlockImpl pred : rb.getPredecessors()) { + for (Block pred : rb.getPredecessors()) { assert currentInput != null : "@AssumeAssertion(nullness): invariant"; propagateStoresTo( pred, @@ -143,7 +143,7 @@ public void performAnalysisBlock(Block b) { .getRegularStore() .leastUpperBound(exceptionStore) : transferResult.getRegularStore(); - for (BlockImpl pred : eb.getPredecessors()) { + for (Block pred : eb.getPredecessors()) { addStoreAfter(pred, node, mergedStore, addToWorklistAgain); } break; @@ -154,7 +154,7 @@ public void performAnalysisBlock(Block b) { TransferInput inputAfter = getInput(cb); assert inputAfter != null : "@AssumeAssertion(nullness): invariant"; TransferInput input = inputAfter.copy(); - for (BlockImpl pred : cb.getPredecessors()) { + for (Block pred : cb.getPredecessors()) { propagateStoresTo(pred, null, input, FlowRule.EACH_TO_EACH, false); } break; @@ -173,7 +173,7 @@ public void performAnalysisBlock(Block b) { || sType == SpecialBlockType.EXCEPTIONAL_EXIT; TransferInput input = getInput(sb); assert input != null : "@AssumeAssertion(nullness): invariant"; - for (BlockImpl pred : sb.getPredecessors()) { + for (Block pred : sb.getPredecessors()) { propagateStoresTo(pred, null, input, FlowRule.EACH_TO_EACH, false); } } @@ -285,6 +285,7 @@ protected void addStoreAfter(Block pred, @Nullable Node node, S s, boolean addBl (exceptionStore != null) ? exceptionStore.leastUpperBound(s) : s; if (!newExceptionStore.equals(exceptionStore)) { exceptionStores.put(ebPred, newExceptionStore); + inputs.put(ebPred, new TransferInput(node, this, newExceptionStore)); addBlockToWorklist = true; } } @@ -314,7 +315,7 @@ protected void addStoreAfter(Block pred, @Nullable Node node, S s, boolean addBl @Override public S runAnalysisFor( - Node node, + @FindDistinct Node node, boolean before, TransferInput transferInput, IdentityHashMap nodeValues, @@ -339,11 +340,12 @@ public S runAnalysisFor( ListIterator reverseIter = nodeList.listIterator(nodeList.size()); while (reverseIter.hasPrevious()) { Node n = reverseIter.previous(); - currentNode = n; + setCurrentNode(n); if (n == node && !before) { return store.getRegularStore(); } - // Copy the store to preserve to change the state in {@link #inputs} + // Copy the store to avoid changing other blocks' transfer inputs in + // {@link #inputs} TransferResult transferResult = callTransferFunction(n, store.copy()); if (n == node) { @@ -368,9 +370,11 @@ public S runAnalysisFor( if (!before) { return transferInput.getRegularStore(); } - currentNode = node; + setCurrentNode(node); + // Copy the store to avoid changing other blocks' transfer inputs in {@link + // #inputs} TransferResult transferResult = - callTransferFunction(node, transferInput); + callTransferFunction(node, transferInput.copy()); // Merge transfer result with the exception store of this exceptional block S exceptionStore = exceptionStores.get(eb); return exceptionStore == null @@ -383,7 +387,7 @@ public S runAnalysisFor( } } finally { - currentNode = oldCurrentNode; + setCurrentNode(oldCurrentNode); isRunning = false; } } diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/analysis/FlowExpressions.java b/dataflow/src/main/java/org/checkerframework/dataflow/analysis/FlowExpressions.java index 6fcfc44abc2..72b5b641c3a 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/analysis/FlowExpressions.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/analysis/FlowExpressions.java @@ -26,6 +26,8 @@ import javax.lang.model.element.VariableElement; import javax.lang.model.type.TypeKind; import javax.lang.model.type.TypeMirror; +import org.checkerframework.checker.interning.qual.EqualsMethod; +import org.checkerframework.checker.interning.qual.UsesObjectEquals; import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.dataflow.cfg.node.ArrayAccessNode; import org.checkerframework.dataflow.cfg.node.ArrayCreationNode; @@ -444,9 +446,23 @@ private static Receiver internalReprOfMemberSelect( return internalArguments; } + // The syntax that the Checker Framework uses for Java expressions also includes "" and + // "#1" for formal parameters. However, there are no special subclasses (AST nodes) for those + // extensions. /** - * The poorly-named Receiver class is actually a Java AST. Each subclass represents a different - * type of expression, such as MethodCall, ArrayAccess, LocalVariable, etc. + * This class represents a Java expression and its type. It does not represent all possible Java + * expressions (for example, it does not represent a ternary expression; use {@link + * FlowExpressions.Unknown} for unrepresentable expressions). + * + *

    This class's representation is like an AST: subparts are also expressions. For declared + * names (fields, local variables, and methods), it also contains an Element. + * + *

    Each subclass represents a different type of expression, such as {@link + * FlowExpressions.MethodCall}, {@link FlowExpressions.ArrayAccess}, {@link + * FlowExpressions.LocalVariable}, etc. + * + * @see the syntax + * of Java expressions supported by the Checker Framework */ public abstract static class Receiver { /** The type of this expression. */ @@ -495,10 +511,12 @@ public boolean containsUnknown() { public abstract boolean isUnmodifiableByOtherCode(); /** - * Returns true if and only if the two receiver are syntactically identical. + * Returns true if and only if the two receivers are syntactically identical. * - * @return true if and only if the two receiver are syntactically identical + * @param other the other object to compare to this one + * @return true if and only if the two receivers are syntactically identical */ + @EqualsMethod public boolean syntacticEquals(Receiver other) { return other == this; } @@ -734,7 +752,14 @@ public boolean containsModifiableAliasOf(Store store, Receiver other) { } } + /** Stands for any expression that the Dataflow Framework lacks explicit support for. */ + @UsesObjectEquals public static class Unknown extends Receiver { + /** + * Create a new Unknown receiver. + * + * @param type the Java type of this receiver + */ public Unknown(TypeMirror type) { super(type); } @@ -1074,11 +1099,14 @@ public boolean containsModifiableAliasOf(Store store, Receiver other) { @Override public boolean equals(@Nullable Object obj) { + if (this == obj) { + return true; + } if (!(obj instanceof MethodCall)) { return false; } if (method.getKind() == ElementKind.CONSTRUCTOR) { - return this == obj; + return false; } MethodCall other = (MethodCall) obj; return parameters.equals(other.parameters) 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 d075f562f68..a35bc78e335 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/analysis/ForwardAnalysisImpl.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/analysis/ForwardAnalysisImpl.java @@ -9,6 +9,7 @@ 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; import org.checkerframework.dataflow.cfg.ControlFlowGraph; @@ -224,7 +225,7 @@ public void performAnalysisBlock(Block b) { } @Override - @SuppressWarnings("contracts.precondition.override.invalid") // implementation field + @SuppressWarnings("nullness:contracts.precondition.override.invalid") // implementation field @RequiresNonNull("cfg") public List>> getReturnStatementStores() { List>> result = new ArrayList<>(); @@ -237,7 +238,7 @@ public void performAnalysisBlock(Block b) { @Override public S runAnalysisFor( - Node node, + @FindDistinct Node node, boolean before, TransferInput transferInput, IdentityHashMap nodeValues, @@ -274,14 +275,15 @@ public S runAnalysisFor( TransferInput store = transferInput; TransferResult transferResult; for (Node n : rb.getContents()) { - currentNode = n; + setCurrentNode(n); if (n == node && before) { return store.getRegularStore(); } if (cache != null && cache.containsKey(n)) { transferResult = cache.get(n); } else { - // Copy the store to preserve to change the state in the cache + // Copy the store to avoid changing other blocks' transfer inputs in + // {@link #inputs} transferResult = callTransferFunction(n, store.copy()); if (cache != null) { cache.put(n, transferResult); @@ -310,9 +312,11 @@ public S runAnalysisFor( if (before) { return transferInput.getRegularStore(); } - currentNode = node; + setCurrentNode(node); + // Copy the store to avoid changing other blocks' transfer inputs in {@link + // #inputs} TransferResult transferResult = - callTransferFunction(node, transferInput); + callTransferFunction(node, transferInput.copy()); return transferResult.getRegularStore(); } default: @@ -320,7 +324,7 @@ public S runAnalysisFor( throw new BugInCF("Unexpected block type: " + block.getType()); } } finally { - currentNode = oldCurrentNode; + setCurrentNode(oldCurrentNode); isRunning = false; } } @@ -502,7 +506,9 @@ protected void addStoreBefore( break; } case BOTH: - if (thenStore == elseStore) { + @SuppressWarnings("interning:not.interned") + boolean sameStore = (thenStore == elseStore); + if (sameStore) { // Currently there is only one regular store S newStore = mergeStores(s, thenStore, shouldWiden); if (!newStore.equals(thenStore)) { diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/analysis/TransferInput.java b/dataflow/src/main/java/org/checkerframework/dataflow/analysis/TransferInput.java index 6682873967d..f6b47e20e3d 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/analysis/TransferInput.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/analysis/TransferInput.java @@ -22,8 +22,8 @@ public class TransferInput, S extends Store> { protected @Nullable Node node; /** - * The regular result store (or {@code null} if none is present). The following invariant is - * maintained: + * The regular result store (or {@code null} if none is present, because {@link #thenStore} and + * {@link #elseStore} are set). The following invariant is maintained: * *

    
          * store == null ⇔ thenStore != null && elseStore != null
    @@ -32,22 +32,14 @@ public class TransferInput, S extends Store> {
         protected final @Nullable S store;
     
         /**
    -     * The 'then' result store (or {@code null} if none is present). The following invariant is
    -     * maintained:
    -     *
    -     * 
    
    -     * store == null ⇔ thenStore != null && elseStore != null
    -     * 
    + * The 'then' result store (or {@code null} if none is present). See invariant at {@link + * #store}. */ protected final @Nullable S thenStore; /** - * The 'else' result store (or {@code null} if none is present). The following invariant is - * maintained: - * - *
    
    -     * store == null ⇔ thenStore != null && elseStore != null
    -     * 
    + * The 'else' result store (or {@code null} if none is present). See invariant at {@link + * #store}. */ protected final @Nullable S elseStore; @@ -221,7 +213,7 @@ public S getElseStore() { * potentially not equal */ public boolean containsTwoStores() { - return (thenStore != null && elseStore != null); + return store == null; } /** diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/AbstractCFGVisualizer.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/AbstractCFGVisualizer.java index e10175c3600..a315e1bf007 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/AbstractCFGVisualizer.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/AbstractCFGVisualizer.java @@ -44,8 +44,9 @@ public abstract class AbstractCFGVisualizer< implements CFGVisualizer { /** - * Initialized in {@link #init(Map)}. If its value is {@code true}, {@link CFGVisualizer} - * returns more detailed information. + * If {@code true}, {@link CFGVisualizer} returns more detailed information. + * + *

    Initialized in {@link #init(Map)}. */ protected boolean verbose; @@ -57,12 +58,24 @@ public abstract class AbstractCFGVisualizer< @Override public void init(Map args) { - Object verb = args.get("verbose"); - this.verbose = - verb != null - && (verb instanceof String - ? Boolean.parseBoolean((String) verb) - : (boolean) verb); + this.verbose = toBoolean(args.get("verbose")); + } + + /** + * Convert the value to boolean, by parsing a string or casting any other value. null converts + * to false. + * + * @param o an object to convert to boolean + * @return {@code o} converted to boolean + */ + private static boolean toBoolean(@Nullable Object o) { + if (o == null) { + return false; + } + if (o instanceof String) { + return Boolean.parseBoolean((String) o); + } + return (boolean) o; } /** @@ -104,12 +117,15 @@ protected String visualizeGraphWithoutHeaderAndFooter( } /** - * Adds the successors of the current block to the work list and the visited blocks list. + * Outputs, to sbGraph, a visualization of a block's edges, but not the block itself. (The block + * itself is output elsewhere.) Also adds the successors of the block to the work list and the + * visited blocks list. * * @param cur the current block - * @param visited the set of blocks that have already been visited or are in the work list - * @param workList the queue of blocks to be processed - * @param sbGraph the {@link StringBuilder} to store the graph + * @param visited the set of blocks that have already been visited or are in the work list; side + * effected by this method + * @param workList the queue of blocks to be processed; side effected by this method + * @param sbGraph the {@link StringBuilder} to store the graph; side effected by this method */ protected void handleSuccessorsHelper( Block cur, Set visited, Queue workList, StringBuilder sbGraph) { @@ -454,12 +470,12 @@ protected abstract String visualizeNodes( /** * Generate the String representation of an edge. * - * @param sId the ID of current block - * @param eId the ID of successor block + * @param sId a representation of the current block, such as its ID + * @param eId a representation of the successor block, such as its ID * @param flowRule the content of the edge * @return the String representation of the edge */ - protected abstract String addEdge(long sId, long eId, String flowRule); + protected abstract String addEdge(Object sId, Object eId, String flowRule); /** * Return the header of the generated graph. @@ -476,15 +492,16 @@ protected abstract String visualizeNodes( protected abstract String visualizeGraphFooter(); /** - * Return the simple String of the process order of a node, e.g., "Process order: 23". When a - * node have multiple process orders, a sequence of numbers will be returned, e.g., "Process - * order: 23,25". + * Given a list of process orders (integers), returns a string representation. + * + *

    Examples: "Process order: 23", "Process order: 23,25". * - * @param order the list of the process order to be processed - * @return the String representation of the process order of the node + * @param order a list of process orders + * @return a String representation of the given process orders */ protected String getProcessOrderSimpleString(List order) { - return "Process order: " + order.toString().replaceAll("[\\[\\]]", ""); + String orderString = order.toString(); + return "Process order: " + orderString.substring(1, orderString.length() - 1); } /** diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/CFGBuilder.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/CFGBuilder.java index f42d5a1d463..2e6dfcf19b0 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/CFGBuilder.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/CFGBuilder.java @@ -91,6 +91,7 @@ import javax.lang.model.type.UnionType; import javax.lang.model.util.Elements; import javax.lang.model.util.Types; +import org.checkerframework.checker.interning.qual.FindDistinct; import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.dataflow.analysis.Store; import org.checkerframework.dataflow.cfg.CFGBuilder.ExtendedNode.ExtendedNodeType; @@ -209,6 +210,7 @@ * preserving the control flow structure. * */ +@SuppressWarnings("nullness") // TODO public class CFGBuilder { /** This class should never be instantiated. Protected to still allow subclasses. */ @@ -281,13 +283,15 @@ public static ControlFlowGraph build( * An extended node can be one of several things (depending on its {@code type}): * *

      - *
    • NODE. An extended node of this type is just a wrapper for a {@link Node} (that - * cannot throw exceptions). - *
    • EXCEPTION_NODE. A wrapper for a {@link Node} which can throw exceptions. It - * contains a label for every possible exception type the node might throw. - *
    • UNCONDITIONAL_JUMP. An unconditional jump to a label. - *
    • TWO_TARGET_CONDITIONAL_JUMP. A conditional jump with two targets for both the - * 'then' and 'else' branch. + *
    • NODE: {@link CFGBuilder.NodeHolder}. An extended node of this type is just a + * wrapper for a {@link Node} (that cannot throw exceptions). + *
    • EXCEPTION_NODE: {@link CFGBuilder.NodeWithExceptionsHolder}. A wrapper for a + * {@link Node} which can throw exceptions. It contains a label for every possible + * exception type the node might throw. + *
    • UNCONDITIONAL_JUMP: {@link CFGBuilder.UnconditionalJump}. An unconditional + * jump to a label. + *
    • TWO_TARGET_CONDITIONAL_JUMP: {@link CFGBuilder.ConditionalJump}. A conditional + * jump with two targets for both the 'then' and 'else' branch. *
    */ protected abstract static class ExtendedNode { @@ -916,9 +920,9 @@ public static ControlFlowGraph process(ControlFlowGraph cfg) { // fix predecessor lists by removing any unreachable predecessors for (Block c : worklist) { BlockImpl cur = (BlockImpl) c; - for (BlockImpl pred : new HashSet<>(cur.getPredecessors())) { + for (Block pred : new HashSet<>(cur.getPredecessors())) { if (!worklist.contains(pred)) { - cur.removePredecessor(pred); + cur.removePredecessor((BlockImpl) pred); } } } @@ -1008,6 +1012,7 @@ public static ControlFlowGraph process(ControlFlowGraph cfg) { * @param predecessors an empty set to be filled by this method with all predecessors * @return the single successor of the set of the empty basic blocks */ + @SuppressWarnings("interning:not.interned") // AST node comparisons protected static BlockImpl computeNeighborhoodOfEmptyBlock( RegularBlockImpl start, Set empty, @@ -1052,7 +1057,8 @@ protected static void computeNeighborhoodOfEmptyBlockBackwards( RegularBlockImpl cur = start; empty.add(cur); - for (final BlockImpl pred : cur.getPredecessors()) { + for (final Block p : cur.getPredecessors()) { + BlockImpl pred = (BlockImpl) p; switch (pred.getType()) { case SPECIAL_BLOCK: // add pred correctly to predecessor list @@ -1087,7 +1093,12 @@ protected static void computeNeighborhoodOfEmptyBlockBackwards( * place where previously the edge pointed to {@code cur}. Additionally, the predecessor * holder also takes care of unlinking (i.e., removing the {@code pred} from {@code cur's} * predecessors). + * + * @param pred a block whose successor should be set + * @param cur the previous successor of {@code pred} + * @return a predecessor holder to set the successor of {@code pred} */ + @SuppressWarnings("interning:not.interned") // AST node comparisons protected static PredecessorHolder getPredecessorHolder( final BlockImpl pred, final BlockImpl cur) { switch (pred.getType()) { @@ -1224,6 +1235,7 @@ private CFGTranslationPhaseTwo() {} * empty regular basic blocks or conditional blocks with the same block as 'then' and * 'else' successor) */ + @SuppressWarnings("interning:not.interned") // AST node comparisons public static ControlFlowGraph process(PhaseOneResult in) { Map bindings = in.bindings; @@ -1899,7 +1911,7 @@ protected void extendWithExtendedNode(ExtendedNode n) { * @param pred the desired predecessor */ @SuppressWarnings("ModifyCollectionInEnhancedForLoop") - protected void insertExtendedNodeAfter(ExtendedNode n, Node pred) { + protected void insertExtendedNodeAfter(ExtendedNode n, @FindDistinct Node pred) { int index = -1; for (int i = 0; i < nodeList.size(); i++) { ExtendedNode inList = nodeList.get(i); @@ -2747,7 +2759,7 @@ public Node visitAssignment(AssignmentTree tree, Void p) { ExpressionTree variable = tree.getVariable(); TypeMirror varType = TreeUtils.typeOf(variable); - // case 1: field access + // case 1: lhs is field access if (TreeUtils.isFieldAccess(variable)) { // visit receiver Node receiver = getReceiver(variable); @@ -2775,7 +2787,7 @@ public Node visitAssignment(AssignmentTree tree, Void p) { extendWithNode(assignmentNode); } - // case 2: other cases + // case 2: lhs is not a field access else { Node target = scan(variable, p); target.setLValue(); diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/CFGVisualizeLauncher.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/CFGVisualizeLauncher.java index b7f95e0401e..d9769aeeb58 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/CFGVisualizeLauncher.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/CFGVisualizeLauncher.java @@ -38,12 +38,11 @@ public class CFGVisualizeLauncher { */ public static void main(String[] args) { CFGVisualizeLauncher cfgVisualizeLauncher = new CFGVisualizeLauncher(); - if (args.length < 2) { + if (args.length == 0) { cfgVisualizeLauncher.printUsage(); System.exit(1); } String input = args[0]; - String output = args[1]; File file = new File(input); if (!file.canRead()) { cfgVisualizeLauncher.printError("Cannot read input file: " + file.getAbsolutePath()); @@ -53,34 +52,48 @@ public static void main(String[] args) { String method = "test"; String clas = "Test"; + String output = "."; boolean pdf = false; boolean error = false; boolean verbose = false; + boolean string = false; - for (int i = 2; i < args.length; i++) { + for (int i = 1; i < args.length; i++) { switch (args[i]) { - case "-pdf": + case "--outputdir": + if (i >= args.length - 1) { + cfgVisualizeLauncher.printError( + "Did not find after --outputdir."); + continue; + } + i++; + output = args[i]; + break; + case "--pdf": pdf = true; break; - case "-method": + case "--method": if (i >= args.length - 1) { - cfgVisualizeLauncher.printError("Did not find after -method."); + cfgVisualizeLauncher.printError("Did not find after --method."); continue; } i++; method = args[i]; break; - case "-class": + case "--class": if (i >= args.length - 1) { - cfgVisualizeLauncher.printError("Did not find after -class."); + cfgVisualizeLauncher.printError("Did not find after --class."); continue; } i++; clas = args[i]; break; - case "-verbose": + case "--verbose": verbose = true; break; + case "--string": + string = true; + break; default: cfgVisualizeLauncher.printError("Unknown command line argument: " + args[i]); error = true; @@ -92,12 +105,19 @@ public static void main(String[] args) { System.exit(1); } - cfgVisualizeLauncher.generateDOTofCFGWithoutAnalysis( - input, output, method, clas, pdf, verbose); + if (!string) { + cfgVisualizeLauncher.generateDOTofCFGWithoutAnalysis( + input, output, method, clas, pdf, verbose); + } else { + String stringGraph = + cfgVisualizeLauncher.generateStringOfCFGWithoutAnalysis( + input, method, clas, verbose); + System.out.println(stringGraph); + } } /** - * Generate the DOT representation of the CFG for a method without analysis. + * Generate the DOT representation of the CFG for a method, only. Does no dataflow analysis. * * @param inputFile java source input file * @param outputDir output directory @@ -116,6 +136,29 @@ protected void generateDOTofCFGWithoutAnalysis( generateDOTofCFG(inputFile, outputDir, method, clas, pdf, verbose, null); } + /** + * Generate the String representation of the CFG for a method, only. Does no dataflow analysis. + * + * @param inputFile java source input file + * @param method name of the method to generate the CFG for + * @param clas name of the class which includes the method to generate the CFG for + * @param verbose show verbose information in CFG + * @return the String representation of the CFG + */ + protected String generateStringOfCFGWithoutAnalysis( + String inputFile, String method, String clas, boolean verbose) { + @Nullable Map res = generateStringOfCFG(inputFile, method, clas, verbose, null); + if (res != null) { + String stringGraph = (String) res.get("stringGraph"); + if (stringGraph == null) { + return "Unexpected output from generating string control flow graph, shouldn't be null."; + } + return stringGraph; + } else { + return "Unexpected output from generating string control flow graph, shouldn't be null."; + } + } + /** * Generate the DOT representation of the CFG for a method. * @@ -270,15 +313,19 @@ protected void producePDF(String file) { /** Print usage information. */ protected void printUsage() { System.out.println( - "Generate the control flow graph of a Java method, represented as a DOT graph."); + "Generate the control flow graph of a Java method, represented as a DOT or String graph."); + System.out.println( + "Parameters: [--outputdir ] [--method ] [--class ] [--pdf] [--verbose] [--string]"); + System.out.println( + " --outputdir: The output directory for the generated files (defaults to '.')."); System.out.println( - "Parameters: [-method ] [-class ] [-pdf] [-verbose]"); - System.out.println(" -pdf: Also generate the PDF by invoking 'dot'."); + " --method: The method to generate the CFG for (defaults to 'test')."); System.out.println( - " -method: The method to generate the CFG for (defaults to 'test')."); + " --class: The class in which to find the method (defaults to 'Test')."); + System.out.println(" --pdf: Also generate the PDF by invoking 'dot'."); + System.out.println(" --verbose: Show the verbose output (defaults to 'false')."); System.out.println( - " -class: The class in which to find the method (defaults to 'Test')."); - System.out.println(" -verbose: Show the verbose output (defaults to 'false')."); + " --string: Print the string representation of the control flow graph (defaults to 'false')."); } /** diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/ControlFlowGraph.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/ControlFlowGraph.java index abff30922fa..7474970a0b3 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/ControlFlowGraph.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/ControlFlowGraph.java @@ -7,6 +7,7 @@ import com.sun.source.tree.UnaryTree; import java.util.ArrayDeque; import java.util.ArrayList; +import java.util.Collection; import java.util.Collections; import java.util.Deque; import java.util.HashMap; @@ -18,9 +19,9 @@ import java.util.Set; import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.dataflow.cfg.block.Block; -import org.checkerframework.dataflow.cfg.block.Block.BlockType; import org.checkerframework.dataflow.cfg.block.ConditionalBlock; import org.checkerframework.dataflow.cfg.block.ExceptionBlock; +import org.checkerframework.dataflow.cfg.block.RegularBlock; import org.checkerframework.dataflow.cfg.block.SingleSuccessorBlock; import org.checkerframework.dataflow.cfg.block.SpecialBlock; import org.checkerframework.dataflow.cfg.block.SpecialBlockImpl; @@ -28,7 +29,14 @@ import org.checkerframework.dataflow.cfg.node.Node; import org.checkerframework.dataflow.cfg.node.ReturnNode; -/** A control flow graph (CFG for short) of a single method. */ +/** + * A control flow graph (CFG for short) of a single method. + * + *

    The graph is represented by the successors (methods {@link SingleSuccessorBlock#getSuccessor}, + * {@link ConditionalBlock#getThenSuccessor}, {@link ConditionalBlock#getElseSuccessor}, {@link + * ExceptionBlock#getExceptionalSuccessors}, {@link RegularBlock#getRegularSuccessor}) and + * predecessors (method {@link Block#getPredecessors}) of the entry and exit blocks. + */ public class ControlFlowGraph { /** The entry block of the control flow graph. */ @@ -162,7 +170,7 @@ public Set getAllBlocks() { break; } - Deque succs = getSuccessors(cur); + Collection succs = cur.getSuccessors(); for (Block b : succs) { if (!visited.contains(b)) { @@ -196,7 +204,7 @@ public List getDepthFirstOrderedBlocks() { worklist.removeLast(); } else { visited.add(cur); - Deque successors = getSuccessors(cur); + Collection successors = cur.getSuccessors(); successors.removeAll(visited); worklist.addAll(successors); } @@ -206,34 +214,6 @@ public List getDepthFirstOrderedBlocks() { return dfsOrderResult; } - /** - * Get a list of all successor Blocks for cur. - * - * @return a Deque of successor Blocks - */ - private Deque getSuccessors(Block cur) { - Deque succs = new ArrayDeque<>(); - if (cur.getType() == BlockType.CONDITIONAL_BLOCK) { - ConditionalBlock ccur = ((ConditionalBlock) cur); - succs.add(ccur.getThenSuccessor()); - succs.add(ccur.getElseSuccessor()); - } else { - assert cur instanceof SingleSuccessorBlock; - Block b = ((SingleSuccessorBlock) cur).getSuccessor(); - if (b != null) { - succs.add(b); - } - } - - if (cur.getType() == BlockType.EXCEPTION_BLOCK) { - ExceptionBlock ecur = (ExceptionBlock) cur; - for (Set exceptionSuccSet : ecur.getExceptionalSuccessors().values()) { - succs.addAll(exceptionSuccSet); - } - } - return succs; - } - /** * Returns the copied tree-lookup map. * diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/DOTCFGVisualizer.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/DOTCFGVisualizer.java index 654a4fe5de0..30446274b9b 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/DOTCFGVisualizer.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/DOTCFGVisualizer.java @@ -1,5 +1,6 @@ package org.checkerframework.dataflow.cfg; +import com.sun.source.tree.VariableTree; import com.sun.tools.javac.tree.JCTree; import java.io.BufferedWriter; import java.io.FileWriter; @@ -9,6 +10,7 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.StringJoiner; import org.checkerframework.checker.nullness.qual.KeyFor; import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.dataflow.analysis.AbstractValue; @@ -16,6 +18,7 @@ import org.checkerframework.dataflow.analysis.FlowExpressions; import org.checkerframework.dataflow.analysis.Store; import org.checkerframework.dataflow.analysis.TransferFunction; +import org.checkerframework.dataflow.cfg.UnderlyingAST.CFGLambda; import org.checkerframework.dataflow.cfg.UnderlyingAST.CFGMethod; import org.checkerframework.dataflow.cfg.UnderlyingAST.CFGStatement; import org.checkerframework.dataflow.cfg.block.Block; @@ -26,7 +29,7 @@ import org.checkerframework.javacutil.UserError; /** Generate a graph description in the DOT language of a control graph. */ -@SuppressWarnings("initialization.fields.uninitialized") // uses init method +@SuppressWarnings("nullness:initialization.fields.uninitialized") // uses init method public class DOTCFGVisualizer< V extends AbstractValue, S extends Store, T extends TransferFunction> extends AbstractCFGVisualizer { @@ -78,7 +81,7 @@ public void init(Map args) { return res; } - @SuppressWarnings("enhancedfor.type.incompatible") + @SuppressWarnings("keyfor:enhancedfor.type.incompatible") @Override public String visualizeNodes( Set blocks, ControlFlowGraph cfg, @Nullable Analysis analysis) { @@ -122,8 +125,15 @@ public String visualizeNodes( } @Override - protected String addEdge(long sId, long eId, String flowRule) { - return " " + sId + " -> " + eId + " [label=\"" + flowRule + "\"];" + lineSeparator; + protected String addEdge(Object sId, Object eId, String flowRule) { + return " " + + format(sId) + + " -> " + + format(eId) + + " [label=\"" + + flowRule + + "\"];" + + lineSeparator; } @Override @@ -180,19 +190,47 @@ protected String dotOutputFileName(UnderlyingAST ast) { CFGMethod cfgMethod = (CFGMethod) ast; String clsName = cfgMethod.getClassTree().getSimpleName().toString(); String methodName = cfgMethod.getMethod().getName().toString(); + StringJoiner params = new StringJoiner(","); + for (VariableTree tree : cfgMethod.getMethod().getParameters()) { + params.add(tree.getType().toString()); + } outFile.append(clsName); outFile.append("-"); outFile.append(methodName); + if (params.length() != 0) { + outFile.append("-"); + outFile.append(params); + } srcLoc.append("<"); srcLoc.append(clsName); srcLoc.append("::"); srcLoc.append(methodName); srcLoc.append("("); - srcLoc.append(cfgMethod.getMethod().getParameters()); + srcLoc.append(params); srcLoc.append(")::"); srcLoc.append(((JCTree) cfgMethod.getMethod()).pos); srcLoc.append(">"); + } else if (ast.getKind() == UnderlyingAST.Kind.LAMBDA) { + CFGLambda cfgLambda = (CFGLambda) ast; + String clsName = cfgLambda.getClassTree().getSimpleName().toString(); + String methodName = cfgLambda.getMethod().getName().toString(); + int hashCode = cfgLambda.getCode().hashCode(); + outFile.append(clsName); + outFile.append("-"); + outFile.append(methodName); + outFile.append("-"); + outFile.append(hashCode); + + srcLoc.append("<"); + srcLoc.append(clsName); + srcLoc.append("::"); + srcLoc.append(methodName); + srcLoc.append("("); + srcLoc.append(cfgLambda.getMethod().getParameters()); + srcLoc.append(")::"); + srcLoc.append(((JCTree) cfgLambda.getCode()).pos); + srcLoc.append(">"); } else { throw new BugInCF("Unexpected AST kind: " + ast.getKind() + " value: " + ast); } diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/StringCFGVisualizer.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/StringCFGVisualizer.java index f257c4dde77..1843c0ab1d9 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/StringCFGVisualizer.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/StringCFGVisualizer.java @@ -31,7 +31,7 @@ public Map visualize( return res; } - @SuppressWarnings("enhancedfor.type.incompatible") + @SuppressWarnings("keyfor:enhancedfor.type.incompatible") @Override public String visualizeNodes( Set blocks, ControlFlowGraph cfg, @Nullable Analysis analysis) { @@ -57,7 +57,7 @@ public String visualizeNodes( } @Override - protected String addEdge(long sId, long eId, String flowRule) { + protected String addEdge(Object sId, Object eId, String flowRule) { if (this.verbose) { return sId + " -> " + eId + " " + flowRule + lineSeparator; } diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/UnderlyingAST.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/UnderlyingAST.java index 73150bc4666..a4c1bf65826 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/UnderlyingAST.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/UnderlyingAST.java @@ -75,11 +75,27 @@ public String toString() { /** If the underlying AST is a lambda. */ public static class CFGLambda extends UnderlyingAST { + /** The lambda expression. */ private final LambdaExpressionTree lambda; - public CFGLambda(LambdaExpressionTree lambda) { + /** The enclosing class of the lambda. */ + private final ClassTree classTree; + + /** The enclosing method of the lambda. */ + private final MethodTree method; + + /** + * Create a new CFGLambda. + * + * @param lambda the lambda expression + * @param classTree the enclosing class of the lambda + * @param method the enclosing method of the lambda + */ + public CFGLambda(LambdaExpressionTree lambda, ClassTree classTree, MethodTree method) { super(Kind.LAMBDA); this.lambda = lambda; + this.method = method; + this.classTree = classTree; } @Override @@ -87,10 +103,33 @@ public Tree getCode() { return lambda.getBody(); } + /** + * Returns the lambda expression tree. + * + * @return the lambda expression tree + */ public LambdaExpressionTree getLambdaTree() { return lambda; } + /** + * Returns the enclosing class of the lambda. + * + * @return the enclosing class of the lambda + */ + public ClassTree getClassTree() { + return classTree; + } + + /** + * Returns the enclosing method of the lambda. + * + * @return the enclosing method of the lambda + */ + public MethodTree getMethod() { + return method; + } + @Override public String toString() { return SystemUtil.joinLines("CFGLambda(", lambda, ")"); diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/block/Block.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/block/Block.java index 2db3de2894c..6f64d33cf92 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/block/Block.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/block/Block.java @@ -40,5 +40,12 @@ public static enum BlockType { * * @return the predecessors of this basic block */ - Set getPredecessors(); + Set getPredecessors(); + + /** + * Returns the successors of this basic block. + * + * @return the successors of this basic block + */ + Set getSuccessors(); } diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/block/BlockImpl.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/block/BlockImpl.java index 1d1961f8487..ce90093297a 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/block/BlockImpl.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/block/BlockImpl.java @@ -7,7 +7,7 @@ /** Base class of the {@link Block} implementation hierarchy. */ public abstract class BlockImpl implements Block { - /** A unique ID for this node. */ + /** A unique ID for this block. */ protected final long id = BlockImpl.uniqueID(); /** The last ID that has already been used. */ @@ -44,7 +44,7 @@ public BlockType getType() { } @Override - public Set getPredecessors() { + public Set getPredecessors() { return Collections.unmodifiableSet(predecessors); } diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/block/ConditionalBlockImpl.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/block/ConditionalBlockImpl.java index bcdde13a59e..a9b4ea0237f 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/block/ConditionalBlockImpl.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/block/ConditionalBlockImpl.java @@ -1,5 +1,7 @@ package org.checkerframework.dataflow.cfg.block; +import java.util.LinkedHashSet; +import java.util.Set; import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.dataflow.analysis.Store; import org.checkerframework.javacutil.BugInCF; @@ -60,6 +62,14 @@ public Block getElseSuccessor() { return elseSuccessor; } + @Override + public Set getSuccessors() { + Set result = new LinkedHashSet<>(2); + result.add(getThenSuccessor()); + result.add(getElseSuccessor()); + return result; + } + @Override public Store.FlowRule getThenFlowRule() { return thenFlowRule; diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/block/ExceptionBlockImpl.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/block/ExceptionBlockImpl.java index 6b871d380b4..261fb36483c 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/block/ExceptionBlockImpl.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/block/ExceptionBlockImpl.java @@ -1,8 +1,8 @@ package org.checkerframework.dataflow.cfg.block; import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; import java.util.Map; import java.util.Set; import javax.lang.model.type.TypeMirror; @@ -22,7 +22,7 @@ public class ExceptionBlockImpl extends SingleSuccessorBlockImpl implements Exce /** Create an empty exceptional block. */ public ExceptionBlockImpl() { super(BlockType.EXCEPTION_BLOCK); - exceptionalSuccessors = new HashMap<>(); + exceptionalSuccessors = new LinkedHashMap<>(); } /** Set the node. */ @@ -43,7 +43,7 @@ public Node getNode() { public void addExceptionalSuccessor(BlockImpl b, TypeMirror cause) { Set blocks = exceptionalSuccessors.get(cause); if (blocks == null) { - blocks = new HashSet<>(); + blocks = new LinkedHashSet<>(); exceptionalSuccessors.put(cause, blocks); } blocks.add(b); @@ -58,6 +58,15 @@ public Map> getExceptionalSuccessors() { return Collections.unmodifiableMap(exceptionalSuccessors); } + @Override + public Set getSuccessors() { + Set result = new LinkedHashSet<>(super.getSuccessors()); + for (Set blocks : getExceptionalSuccessors().values()) { + result.addAll(blocks); + } + return result; + } + @Override public String toString() { return "ExceptionBlock(" + node + ")"; 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 d39152bd7cd..a5d1ac9281e 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 @@ -8,9 +8,11 @@ public interface SingleSuccessorBlock extends Block { /** - * Returns the non-exceptional successor block, or {@code null} if there is no successor. + * Returns the non-exceptional successor block, or {@code null} if there is no non-exceptional + * successor. * - * @return the non-exceptional successor block, or {@code null} if there is no successor + * @return the non-exceptional successor block, or {@code null} if there is no non-exceptional + * successor */ @Pure @Nullable Block getSuccessor(); diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/block/SingleSuccessorBlockImpl.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/block/SingleSuccessorBlockImpl.java index 073c889b11f..2b3e5d553f7 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/block/SingleSuccessorBlockImpl.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/block/SingleSuccessorBlockImpl.java @@ -1,5 +1,7 @@ package org.checkerframework.dataflow.cfg.block; +import java.util.LinkedHashSet; +import java.util.Set; import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.dataflow.analysis.Store; @@ -24,7 +26,20 @@ protected SingleSuccessorBlockImpl(BlockType type) { return successor; } - /** Set a basic block as the successor of this block. */ + @Override + public Set getSuccessors() { + Set result = new LinkedHashSet<>(); + if (successor != null) { + result.add(successor); + } + return result; + } + + /** + * Set a basic block as the successor of this block. + * + * @param successor the block that will be the successor of this + */ public void setSuccessor(BlockImpl successor) { this.successor = successor; successor.addPredecessor(this); diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/util/AbstractMostlySingleton.java b/dataflow/src/main/java/org/checkerframework/dataflow/util/AbstractMostlySingleton.java index 6c689d1d50f..fb2b69db43c 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/util/AbstractMostlySingleton.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/util/AbstractMostlySingleton.java @@ -9,10 +9,13 @@ import org.checkerframework.checker.nullness.qual.PolyNull; import org.checkerframework.javacutil.BugInCF; -/** Base class for sets that are more efficient than HashSet for 0 and 1 elements. */ +/** + * Base class for arbitrary-size sets that are very efficient (more efficient than HashSet) for 0 + * and 1 elements. + */ public abstract class AbstractMostlySingleton implements Set { - /** The possible states of the collection. */ + /** The possible states of a set. */ public enum State { /** An empty set. */ EMPTY, @@ -88,7 +91,8 @@ public T next() { @Override public void remove() { - throw new UnsupportedOperationException(); + state = State.EMPTY; + value = null; } }; case ANY: diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/util/IdentityMostlySingleton.java b/dataflow/src/main/java/org/checkerframework/dataflow/util/IdentityMostlySingleton.java index fa3734f4c29..58b7e4bad69 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/util/IdentityMostlySingleton.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/util/IdentityMostlySingleton.java @@ -1,11 +1,12 @@ package org.checkerframework.dataflow.util; import java.util.ArrayList; +import org.checkerframework.checker.interning.qual.FindDistinct; import org.checkerframework.javacutil.BugInCF; /** - * A set that is more efficient than HashSet for 0 and 1 elements. Uses objects identity for object - * comparison and an {@link ArrayList} for backing storage. + * An arbitrary-size set that is very efficient (more efficient than HashSet) for 0 and 1 elements. + * Uses object identity for object comparison. */ public final class IdentityMostlySingleton extends AbstractMostlySingleton { @@ -21,14 +22,18 @@ public IdentityMostlySingleton(T value) { @Override @SuppressWarnings("fallthrough") - public boolean add(T e) { + public boolean add(@FindDistinct T e) { switch (state) { case EMPTY: state = State.SINGLETON; value = e; return true; case SINGLETON: + if (value == e) { + return false; + } state = State.ANY; + // Use ArrayList, but make sure to use reference comparisons. set = new ArrayList<>(); assert value != null : "@AssumeAssertion(nullness): previous add is non-null"; set.add(value); @@ -36,12 +41,18 @@ public boolean add(T e) { // fallthrough case ANY: assert set != null : "@AssumeAssertion(nullness): set initialized before"; + for (T x : set) { + if (x == e) { + return false; + } + } return set.add(e); default: throw new BugInCF("Unhandled state " + state); } } + @SuppressWarnings("interning:not.interned") // this class uses object identity @Override public boolean contains(Object o) { switch (state) { @@ -51,7 +62,12 @@ public boolean contains(Object o) { return o == value; case ANY: assert set != null : "@AssumeAssertion(nullness): set initialized before"; - return set.contains(o); + for (T x : set) { + if (x == o) { + return true; + } + } + return false; default: throw new BugInCF("Unhandled state " + state); } diff --git a/dataflow/tests/issue3447/Test.java b/dataflow/tests/issue3447/Test.java new file mode 100644 index 00000000000..563d01a1d62 --- /dev/null +++ b/dataflow/tests/issue3447/Test.java @@ -0,0 +1,12 @@ +// Test case for Issue 3447: +// https://github.com/typetools/checker-framework/issues/3447 + +public class Test { + public void test() throws Exception { + try { + int[] myNumbers = {1}; + System.out.println(myNumbers[1]); + } catch (Exception e) { + } + } +} diff --git a/docs/checker-framework-webpage.html b/docs/checker-framework-webpage.html index c30dcd08855..f1a7c3fced1 100644 --- a/docs/checker-framework-webpage.html +++ b/docs/checker-framework-webpage.html @@ -27,12 +27,11 @@

    The Checker Framework

    • Quick start: see the - Installation - instructions and tutorial. + Installation instructions and tutorial.
    • - Download: checker-framework-3.5.0.zip - (1 Jul 2020); + Download: checker-framework-3.6.0.zip + (3 Aug 2020); includes source, platform-independent binary, tests, and documentation.
      Then, see the installation @@ -94,7 +93,7 @@

      The Checker Framework

      the .class file. The tools support both Java 5 declaration annotations and Java 8 type annotations.
        -
      • annotation-tools-3.9.10.zip (01 Jul 2020) +
      • annotation-tools-3.9.11.zip (03 Aug 2020)
      • source code repository
      • @@ -224,7 +223,7 @@

        Mailing lists


        -Last updated: 1 Jul 2020 +Last updated: 3 Aug 2020

        diff --git a/docs/developer/gsoc-ideas.html b/docs/developer/gsoc-ideas.html index e0d1c158b93..4f04689ae22 100644 --- a/docs/developer/gsoc-ideas.html +++ b/docs/developer/gsoc-ideas.html @@ -5,11 +5,11 @@ + "../logo/Checkmark/CFCheckmark_favicon.png"> -Checker Framework logo +Checker Framework logo

        GSoC ideas 2020

        @@ -203,8 +203,8 @@

        How to get started: do a case study

        to suppress a warning. Convince yourself that both branches can execute, or else don't add the if statement.
      • If you add a @SuppressWarnings annotation, - write - it on the smallest possible scope and + write + it on the smallest possible scope and explain why the checker warning is a false positive and you are certain the code is safe. diff --git a/docs/developer/release/maven-artifacts/checker-pom.xml b/docs/developer/release/maven-artifacts/checker-pom.xml index 564ca7a53e6..14593e721ae 100644 --- a/docs/developer/release/maven-artifacts/checker-pom.xml +++ b/docs/developer/release/maven-artifacts/checker-pom.xml @@ -33,6 +33,8 @@ + + mernst Michael Ernst diff --git a/docs/developer/release/maven-artifacts/checker-qual-android-pom.xml b/docs/developer/release/maven-artifacts/checker-qual-android-pom.xml index f637f980365..ee6247eb739 100644 --- a/docs/developer/release/maven-artifacts/checker-qual-android-pom.xml +++ b/docs/developer/release/maven-artifacts/checker-qual-android-pom.xml @@ -39,6 +39,8 @@ + + mernst Michael Ernst diff --git a/docs/developer/release/maven-artifacts/checker-qual-pom.xml b/docs/developer/release/maven-artifacts/checker-qual-pom.xml index bf6a3cbd532..22112d238c2 100644 --- a/docs/developer/release/maven-artifacts/checker-qual-pom.xml +++ b/docs/developer/release/maven-artifacts/checker-qual-pom.xml @@ -33,6 +33,8 @@ + + mernst Michael Ernst diff --git a/docs/developer/release/maven-artifacts/dataflow-pom.xml b/docs/developer/release/maven-artifacts/dataflow-pom.xml index b832a1a8375..8f3391e581e 100644 --- a/docs/developer/release/maven-artifacts/dataflow-pom.xml +++ b/docs/developer/release/maven-artifacts/dataflow-pom.xml @@ -43,6 +43,8 @@ + + mernst Michael Ernst diff --git a/docs/developer/release/maven-artifacts/dataflow-shaded-pom.xml b/docs/developer/release/maven-artifacts/dataflow-shaded-pom.xml index 1dc75c1d323..f5a7a5c155d 100644 --- a/docs/developer/release/maven-artifacts/dataflow-shaded-pom.xml +++ b/docs/developer/release/maven-artifacts/dataflow-shaded-pom.xml @@ -30,6 +30,8 @@ + + mernst Michael Ernst diff --git a/docs/developer/release/maven-artifacts/framework-test-pom.xml b/docs/developer/release/maven-artifacts/framework-test-pom.xml index f6abca146cf..504deb8a4be 100644 --- a/docs/developer/release/maven-artifacts/framework-test-pom.xml +++ b/docs/developer/release/maven-artifacts/framework-test-pom.xml @@ -44,6 +44,8 @@ + + mernst Michael Ernst diff --git a/docs/developer/release/maven-artifacts/javacutil-pom.xml b/docs/developer/release/maven-artifacts/javacutil-pom.xml index 3eb1929b3cb..4f6b16fa2b1 100644 --- a/docs/developer/release/maven-artifacts/javacutil-pom.xml +++ b/docs/developer/release/maven-artifacts/javacutil-pom.xml @@ -50,6 +50,8 @@ + + mernst Michael Ernst diff --git a/docs/examples/MavenExample/pom.xml b/docs/examples/MavenExample/pom.xml index f3b7f18dd50..90802b228eb 100644 --- a/docs/examples/MavenExample/pom.xml +++ b/docs/examples/MavenExample/pom.xml @@ -14,7 +14,7 @@ UTF-8 ${com.google.errorprone:javac:jar} - 3.5.0 + 3.6.0 @@ -108,10 +108,10 @@ -Aquals=myPackage.qual.MyFenum,myPackage.qual.MyCustomType --> -Alint - - - - + + + + diff --git a/docs/examples/MavenExampleJDK11/pom.xml b/docs/examples/MavenExampleJDK11/pom.xml index 80c245f62d9..a8fe50473e8 100644 --- a/docs/examples/MavenExampleJDK11/pom.xml +++ b/docs/examples/MavenExampleJDK11/pom.xml @@ -12,7 +12,7 @@ UTF-8 - 3.5.0 + 3.6.0 diff --git a/docs/examples/fenum-extension/Expected.txt b/docs/examples/fenum-extension/Expected.txt index 87d3cb16063..71361f47cc4 100644 --- a/docs/examples/fenum-extension/Expected.txt +++ b/docs/examples/fenum-extension/Expected.txt @@ -43,12 +43,12 @@ Demo.java:29: error: [assignment.type.incompatible] incompatible types in assign ^ found : @Fenum("B") int required: @Fenum("A") int -Demo.java:32: error: [argument.type.incompatible] incompatible types in argument. +Demo.java:32: error: [argument.type.incompatible] incompatible argument for parameter p of fenumArg. fenumArg(5); // Direct use of value forbidden! ^ found : @FenumUnqualified int required: @Fenum("A") int -Demo.java:33: error: [argument.type.incompatible] incompatible types in argument. +Demo.java:33: error: [argument.type.incompatible] incompatible argument for parameter p of fenumArg. fenumArg(TestStatic.BCONST1); // Incompatible fenums forbidden! ^ found : @Fenum("B") int @@ -63,12 +63,12 @@ Demo.java:37: error: [assignment.type.incompatible] incompatible types in assign ^ found : @Fenum("A") int required: @MyFenum int -Demo.java:40: error: [argument.type.incompatible] incompatible types in argument. +Demo.java:40: error: [argument.type.incompatible] incompatible argument for parameter p of myFenumArg. myFenumArg(8); // Direct use of value forbidden! ^ found : @FenumUnqualified int required: @MyFenum int -Demo.java:41: error: [argument.type.incompatible] incompatible types in argument. +Demo.java:41: error: [argument.type.incompatible] incompatible argument for parameter p of myFenumArg. myFenumArg(TestStatic.BCONST2); // Incompatible fenums forbidden! ^ found : @Fenum("B") int diff --git a/docs/examples/lombok/Makefile b/docs/examples/lombok/Makefile index 5003a44887b..f01c4d37dd1 100644 --- a/docs/examples/lombok/Makefile +++ b/docs/examples/lombok/Makefile @@ -4,7 +4,7 @@ # So check for both the error message and make sure it is for the right assignment. all: clean - ../../../gradlew build > Out.txt 2>&1 - (grep -qF "User.java:9: error: [argument.type.incompatible] incompatible types in argument." Out.txt \ + (grep -qF "User.java:9: error: [argument.type.incompatible] incompatible argument for parameter y of y." Out.txt \ && grep -qF "Foo.java:12: error: [assignment.type.incompatible] incompatible types in assignment." Out.txt \ && grep -qF "y = null; // error" Out.txt) \ || (echo "===== start of Out.txt =====" && cat Out.txt && echo "===== end of Out.txt =====" && false) diff --git a/docs/examples/subtyping-extension/Expected.txt b/docs/examples/subtyping-extension/Expected.txt index 6358c8f797d..8370787c3cd 100644 --- a/docs/examples/subtyping-extension/Expected.txt +++ b/docs/examples/subtyping-extension/Expected.txt @@ -1,7 +1,7 @@ Demo.java:13: warning: [cast.unsafe] cast from "@PossiblyUnencrypted String" to "@Encrypted String" cannot be statically verified return (@Encrypted String) new String(b); ^ -Demo.java:36: error: [argument.type.incompatible] incompatible types in argument. +Demo.java:36: error: [argument.type.incompatible] incompatible argument for parameter msg of sendOverTheInternet. sendOverTheInternet(password); // invalid ^ found : @PossiblyUnencrypted String diff --git a/docs/manual/accumulation-checker.tex b/docs/manual/accumulation-checker.tex index ddb0132e280..949d015e9df 100644 --- a/docs/manual/accumulation-checker.tex +++ b/docs/manual/accumulation-checker.tex @@ -55,6 +55,21 @@ \href{https://github.com/typetools/checker-framework/blob/master/framework/src/test/java/testaccumulation/qual/TestAccumulationBottom.java}{TestAccumulationBottom.java}. It should take no arguments, and should be a subtype of the accumulator type you defined earlier. +You may also define a predicate annotation, analogous to +\href{https://github.com/typetools/checker-framework/blob/master/framework/src/test/java/testaccumulation/qual/TestAccumulationPredicate.java}{TestAccumulationPredicate.java}. +It must have a single argument named \ of type \. +The predicate syntax supports +\begin{itemize} +\item \<||> disjunctions +\item \<\&\&> conjunctions +\item \ logical complement. \<"!x"> means +``it is not true that \ was definitely accumulated'' or, equivalently, ``there is no path on which \ was accumulated''. +Note that this does \textbf{not} mean ``\ was not accumulated'' --- it is not a violation of the specification \<"!x"> if \ is accumulated +on some paths, but not others. +\item \<(...)> parentheses for precedence +\end{itemize} + + \paragraphAndLabel{Setting up the checker}{accumulation-setup} Define a new class that extends \refclass{common/accumulation}{AccumulationChecker}. @@ -62,8 +77,9 @@ Define a new class that extends \refclass{common/accumulation}{AccumulationAnnotatedTypeFactory}. You must create a new constructor whose only argument is a \refclass{common/basetype}{BaseTypeChecker}. -Your constructor should call the \ constructor defined in -\refclass{common/accumulation}{AccumulationAnnotatedTypeFactory}. +Your constructor should call one of the \ constructors defined in +\refclass{common/accumulation}{AccumulationAnnotatedTypeFactory} (which one depends on whether or not +you defined a predicate annotation). \paragraphAndLabel{Adding accumulation logic}{accumulation-accumulating} diff --git a/docs/manual/advanced-features.tex b/docs/manual/advanced-features.tex index d3867b8dd00..69b3f558a78 100644 --- a/docs/manual/advanced-features.tex +++ b/docs/manual/advanced-features.tex @@ -96,7 +96,9 @@ This means that \<@B MyClass> is an invalid type. (Annotations on class declarations may also specify default annotations for uses of the type; see Section~\ref{default-for-use}) -An upper bound can also be specified by the type-system designer using the meta-annotation +If it is not possible to annotate the class's definition (e.g., for +primitives and some library classes), +the type-system designer can specify an upper bound by using the meta-annotation \refqualclass{framework/qual}{UpperBoundFor}. If no annotation is present on a type declaration and if no \<@UpperBoundFor> mentions the type, then @@ -128,8 +130,8 @@ These operations might refine its type. If a user wishes to annotate a method that does type refinement, its formal parameter must be of illegal type \<@A MyClass>, which requires a warning suppression. -If the framework forbid expressions and local variables from having types inconsistent with the class annotation, -then important APIs and common coding paradigms will no longer type-check. +If the framework were to forbid expressions and local variables from having types inconsistent with the class annotation, +then important APIs and common coding paradigms would no longer type-check. Consider the annotation \begin{Verbatim} @@ -225,7 +227,7 @@ this is usually CLIMB-to-top (Section~\ref{climb-to-top}) (CLIMB means \textbf{C}asts, \textbf{L}ocals, \textbf{I}nstanceof, and (some) i\textbf{M}plicit \textbf{B}ounds). \item - The qualifier with the meta-annotation \<@DefaultQualifierInHierarchy> + The qualifier with the meta-annotation \refqualclass{framework/qual}{DefaultQualifierInHierarchy}. \end{enumerate} If the unannotated type is the type of a local variable, then the first 5 rules are skipped and only @@ -1140,7 +1142,9 @@ \item \refqualclass{checker/lock/qual}{Holding} \end{itemize} -The set of permitted expressions is a subset of all Java expressions: +The set of permitted expressions is a subset of all Java expressions, +with a few extensions, formal parameters like \<\#1> and (for some type +systems) \code{}. \begin{itemize} \item @@ -1246,20 +1250,12 @@ \textbf{Limitations:} -The following Java expressions may not currently be written: -% The Checker Framework is best at reasoning about Java expressions that -% are variable references, but these expressions are not. -\begin{itemize} -\item String concatenation expressions. -\item Mathematical operators (plus, minus, division, ...). -\item Comparisons (equality, less than, etc.). -\end{itemize} - -Additionally, it is not possible to write -quantification over all array components (e.g. to express that all +It is not possible to write a +quantification over all array components (e.g., to express that all array elements are non-null). There is no such Java expression, but it would be useful when writing specifications. + \sectionAndLabel{Field invariants}{field-invariants} Sometimes a field declared in a superclass has a more precise type in a diff --git a/docs/manual/annotating-libraries.tex b/docs/manual/annotating-libraries.tex index cb4012a224c..e61332c795e 100644 --- a/docs/manual/annotating-libraries.tex +++ b/docs/manual/annotating-libraries.tex @@ -189,9 +189,29 @@ Whenever you annotate any part of a file, fully annotate the file! That is, write annotations for all the methods and fields, based on their -documentation. If you don't annotate the whole file, then users may be -surprised that calls to some methods type-check as expected whereas other -methods do not (because they have not been annotated). +documentation. Here are reasons for this rule: + +\begin{itemize} +\item + If you annotate just part of the file, then users may be surprised that + calls to some methods type-check as expected whereas other methods do not + (because they have not been annotated). +\item + Annotating one method or field at a time may lead to inconsistencies + between different parts of the file. Different people may make different + assumptions, might write annotations in a way that is locally convenient + but globally inconsistent, or might not read all the documentation of the + class to understand how it works. +\item + It is not much more effort to annotate an entire class versus one method + or field. In either case it is usually necessary to understand the entire + class's design and implementation. Once you have done that, you might as + well annotate the whole thing. +\item + If you fully annotate the file, it is possible to type-check the library to + verify the annotations. (Even if you do not do this right now, it eases + the task in the future.) +\end{itemize} \subsectionAndLabel{Verify your annotations}{library-tips-verify} @@ -367,7 +387,7 @@ annotation. For classes without \<@AnnotatedFor>, the checker uses conservative defaults (see Section~\ref{defaults-classfile}) for any type use with no explicit -user-written annotation, and the checker issues no warnings. +user-written annotation, \emph{and} the checker issues no warnings. \end{sloppypar} The \refqualclass{framework/qual}{AnnotatedFor} annotation, written on a @@ -553,14 +573,11 @@ \subsectionAndLabel{Creating a stub file}{stub-creating} -Stub files are generally stored together with the checker implementation, -in the same directory as the checker's \<.java> source code. - \subsubsectionAndLabel{If you have access to the Java source code}{stub-creating-with-source} Every Java file is a stub file. If you have access to the Java file, -rename file \ to \. You can add +copy file \ to \. You can add annotations to the signatures, leaving the method bodies unchanged. The stub file parser silently ignores any annotations that it cannot resolve to a type, so don't forget the \ statement. @@ -625,7 +642,8 @@ \begin{Verbatim} cd nullness-stub - java -cp $CHECKERFRAMEWORK/checker/dist/checker.jar org.checkerframework.framework.stub.StubGenerator java.lang.String > String.astub + java -cp $CHECKERFRAMEWORK/checker/dist/checker.jar \ + org.checkerframework.framework.stub.StubGenerator java.lang.String > String.astub \end{Verbatim} Supply it with the fully-qualified name of the class for which you wish to @@ -661,6 +679,30 @@ \end{enumerate} +\subsectionAndLabel{Distributing stub files}{stub-creating} + +If you are writing stub files but are not writing a checker, you can place +the stub files anywhere that is convenient. However, please consider +providing your stub files to the checker author, so that other users can +also benefit from the stub files you wrote. + +If you are distributing stub files with a checker implementation, the stub +files appear in the same directory as the checker class, which is a subtype of +\. A \refqualclass{framework/qual}{StubFiles} annotation +on the checker class lists stub files that are always used. For stub files +whose use is optional (for example, because the behavior is unsound, or +unsound except in certain circumstances), users must supply the +\<-Astubs=...> command-line option. + +If a stub file contains annotations that are used by the framework rather +than by any specific checker (such as purity annotations), and you wish to +distribute it with the Checker Framework, put the stub file in directory +\. You can also do this if the stub file has +annotations for multiple checkers. To use a stub file in directory +\, users must supply the +\<-Astubs=checker.jar/stub-file-name.astub> command-line option. + + \subsectionAndLabel{Troubleshooting stub libraries}{stub-troubleshooting} diff --git a/docs/manual/contributors.tex b/docs/manual/contributors.tex index 1f21d095b1e..70f76e9b002 100644 --- a/docs/manual/contributors.tex +++ b/docs/manual/contributors.tex @@ -73,13 +73,16 @@ Nhat Nguyen, Nikhil Shinde, Nima Karimipour, +Nitin Kumar Das, Oleg Shchelykalnov, +Olek Wojnar, Pascal Wittmann, Patrick Meiring, Paul Vines, Paulo Barros, Philip Lai, Prionti Nasir, +Priti Chattopadhyay, Rashmi Mudduluru, Ravi Roshan, Renato Athaydes, diff --git a/docs/manual/creating-a-checker.tex b/docs/manual/creating-a-checker.tex index 2c7991da151..1c189236d05 100644 --- a/docs/manual/creating-a-checker.tex +++ b/docs/manual/creating-a-checker.tex @@ -1168,7 +1168,7 @@ The annotated type of expressions and types are defined via type introduction rules in the type factory. For most expressions and types, these rules are the same regardless of the type system. -For example, the type of a method invocation expression is the return type of the invoked method +For example, the type of a method invocation expression is the return type of the invoked method, viewpoint-adapted for the call site. The framework implements these rules so that all type systems automatically use them. For other expressions, such as string literals, their (annotated) types depend on the type system, so the framework provides way to specify what qualifiers should apply to these expressions. @@ -1783,6 +1783,8 @@ already reported the bug, and you want to continue using the checker on a large codebase without being inundated in stack traces. +\item \code{-AdumpOnErrors}: Outputs a stack trace when reporting errors or warnings. + \end{itemize} @@ -1905,17 +1907,23 @@ The graph also contains information about flow-sensitively refined types of various expressions at many program points. - The argument is a comma-separated sequence of values or key-value pairs. + The argument is a comma-separated sequence of keys or key--value pairs. The first argument is the fully-qualified name of the \ implementation - that should be used. The remaining values or key-value pairs are - passed to \. + that should be used. The remaining keys or key--value pairs are + passed to \. Supported keys include + \begin{itemize} + \item \ + \item \ directory into which to write files + \item \ + \end{itemize} \end{itemize} \noindent You can also use \refclass{dataflow/cfg}{CFGVisualizeLauncher} to generate a DOT or String representation of the control flow graph of a given method in a given class. +The CFG is generated and output, but no dataflow analysis is performed. \begin{itemize} @@ -1926,7 +1934,7 @@ java -Xbootclasspath/p:$CHECKERFRAMEWORK/checker/dist/javac.jar \ -cp $CHECKERFRAMEWORK/checker/dist/checker.jar \ org.checkerframework.dataflow.cfg.CFGVisualizeLauncher \ - MyClass.java output/ -class MyClass -method test -pdf + MyClass.java --class MyClass --method test --pdf \end{Verbatim} \end{smaller} @@ -1937,7 +1945,7 @@ \begin{Verbatim} java -cp $CHECKERFRAMEWORK/checker/dist/checker.jar \ org.checkerframework.dataflow.cfg.CFGVisualizeLauncher \ - MyClass.java output/ -class MyClass -method test -pdf + MyClass.java --class MyClass --method test --pdf \end{Verbatim} \end{smaller} @@ -1945,9 +1953,22 @@ \noindent The above command will generate the corresponding dot and pdf files for the -method \code{test} in the class \code{MyClass} in the directory \. For more -information, run \refclass{dataflow/cfg}{CFGVisualizeLauncher} with no arguments to -see the usage. +method \code{test} in the class \code{MyClass} in the project directory. +To generate a string representation of the graph to standard output, +remove \<--pdf> but add \<--string>. For example (with JDK 8): + +\begin{smaller} +\begin{Verbatim} +java -Xbootclasspath/p:$CHECKERFRAMEWORK/checker/dist/javac.jar \ + -cp $CHECKERFRAMEWORK/checker/dist/checker.jar \ + org.checkerframework.dataflow.cfg.CFGVisualizeLauncher \ + MyClass.java --class MyClass --method test --string +\end{Verbatim} +\end{smaller} + +For more information, run \refclass{dataflow/cfg}{CFGVisualizeLauncher} with +no arguments to see the usage. + \subsectionAndLabel{Miscellaneous debugging options}{creating-debugging-options-misc} diff --git a/docs/manual/external-tools.tex b/docs/manual/external-tools.tex index d243e86089c..57a6cde5bac 100644 --- a/docs/manual/external-tools.tex +++ b/docs/manual/external-tools.tex @@ -117,7 +117,7 @@ \begin{Verbatim} dependencies { ... existing dependencies... - ext.checkerFrameworkVersion = '3.5.0' + ext.checkerFrameworkVersion = '3.6.0' implementation "org.checkerframework:checker-qual-android:${checkerFrameworkVersion}" // or if you use no annotations in source code the above line could be // compileOnly "org.checkerframework:checker-qual-android:${checkerFrameworkVersion}" @@ -190,7 +190,7 @@ \begin{Verbatim} dependencies { ... existing dependencies... - ext.checkerFrameworkVersion = '3.5.0' + ext.checkerFrameworkVersion = '3.6.0' implementation "org.checkerframework:checker-qual-android:${checkerFrameworkVersion}" // or if you use no annotations in source code the above line could be // compileOnly "org.checkerframework:checker-qual-android:${checkerFrameworkVersion}" @@ -223,14 +223,10 @@ %END LATEX \begin{Verbatim} - - - - + - @@ -268,6 +264,8 @@ %END LATEX Fill in each ellipsis (\ldots) from the original compilation target. +However, do not copy any \ setting from the original \code{} +task invocation. In the example, the target is named \code{check-nullness}, but you can name it whatever you like. @@ -340,13 +338,13 @@ \begin{Verbatim} prebuilt_jar( name = 'checker-framework', - binary_jar = 'checker-3.5.0.jar', + binary_jar = 'checker-3.6.0.jar', visibility = [ 'PUBLIC' ] ) prebuilt_jar( name = 'checker-qual', - binary_jar = 'checker-qual-3.5.0.jar', + binary_jar = 'checker-qual-3.6.0.jar', visibility = [ 'PUBLIC' ] ) @@ -378,21 +376,21 @@ use the last one. % Is the last one required for Cygwin, as well as for the Windows command shell? Adjust the pathnames if you have installed the Checker Framework somewhere -other than \<\${HOME}/checker-framework-3.5.0/>. +other than \<\${HOME}/checker-framework-3.6.0/>. \begin{itemize} \item Option 1: Add directory - \code{.../checker-framework-3.5.0/checker/bin} to your path, \emph{before} any other + \code{.../checker-framework-3.6.0/checker/bin} to your path, \emph{before} any other directory that contains a \ executable. If you are using the bash shell, a way to do this is to add the following to your \verb|~/.profile| (or alternately \verb|~/.bash_profile| or \verb|~/.bashrc|) file: \begin{Verbatim} - export CHECKERFRAMEWORK=${HOME}/checker-framework-3.5.0 + export CHECKERFRAMEWORK=${HOME}/checker-framework-3.6.0 export PATH=${CHECKERFRAMEWORK}/checker/bin:${PATH} \end{Verbatim} @@ -413,7 +411,7 @@ file: % No Windows example because this doesn't work under Windows. \begin{Verbatim} - export CHECKERFRAMEWORK=${HOME}/checker-framework-3.5.0 + export CHECKERFRAMEWORK=${HOME}/checker-framework-3.6.0 alias javacheck='$CHECKERFRAMEWORK/checker/bin/javac' \end{Verbatim} @@ -435,11 +433,11 @@ \begin{Verbatim} # Unix - export CHECKERFRAMEWORK=${HOME}/checker-framework-3.5.0 + export CHECKERFRAMEWORK=${HOME}/checker-framework-3.6.0 alias javacheck='java -jar "$CHECKERFRAMEWORK/checker/dist/checker.jar"' # Windows - set CHECKERFRAMEWORK = C:\Program Files\checker-framework-3.5.0\ + set CHECKERFRAMEWORK = C:\Program Files\checker-framework-3.6.0\ doskey javacheck=java -jar "%CHECKERFRAMEWORK%\checker\dist\checker.jar" $* \end{Verbatim} @@ -518,8 +516,8 @@ \begin{itemize} \item \: \url{https://search.maven.org/artifact/com.google.errorprone/javac/9%2B181-r4173-1/jar} -\item \: \url{https://search.maven.org/artifact/org.checkerframework/checker-qual/3.5.0/jar} -\item \: \url{https://search.maven.org/artifact/org.checkerframework/checker/3.5.0/jar} +\item \: \url{https://search.maven.org/artifact/org.checkerframework/checker-qual/3.6.0/jar} +\item \: \url{https://search.maven.org/artifact/org.checkerframework/checker/3.6.0/jar} \end{itemize} Different arguments to \ are required for JDK 8 @@ -998,9 +996,9 @@ \end{enumerate} -\subsectionAndLabel{Maven, with a locally-modified version of the Checker Framework}{maven-local-modifications} +\subsectionAndLabel{Maven, with a locally-built version of the Checker Framework}{maven-locally-built} -To use a locally-modified version of the Checker Framework, first run: +To use a locally-built version of the Checker Framework, first run: \begin{alltt} ./gradlew deployArtifactsToLocalRepo @@ -1082,14 +1080,10 @@ %END LATEX \begin{Verbatim} - - - - + - diff --git a/docs/manual/figures/chainlink.svg b/docs/manual/figures/chainlink.svg index 88fa283e374..bf88e803b2e 100644 --- a/docs/manual/figures/chainlink.svg +++ b/docs/manual/figures/chainlink.svg @@ -2,46 +2,16 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + width="482.136px" height="482.135px" viewBox="0 0 482.136 482.135" style="enable-background:new 0 0 482.136 482.135;" + xml:space="preserve"> + + diff --git a/docs/manual/generics.tex b/docs/manual/generics.tex index 27a4d158b02..0523fc99962 100644 --- a/docs/manual/generics.tex +++ b/docs/manual/generics.tex @@ -445,16 +445,17 @@ invariantly. For example, \code{List<@Nullable String>} is not a subtype of \code{List}. -When a type parameter is used in a read-only way --- that is, when values -of that type are read but are never assigned --- then it is safe for the +When a type parameter is used in a read-only way --- that is, when clients +read values of that type from the class but never pass values of that type +to the class --- then it is safe for the type to be \emph{covariant} in the type parameter. Use the \refqualclass{framework/qual}{Covariant} annotation to indicate this. When a type parameter is covariant, two instantiations of the class with different type arguments have the same subtyping relationship as the type arguments do. -For example, consider \. Its elements can be read but not -written, so \code{Iterator<@Nullable String>} can be a subtype of +For example, consider \. A client can read elements but not +write them, so \code{Iterator<@Nullable String>} can be a subtype of \code{Iterator} without introducing a hole in the type system. Therefore, its type parameter is annotated with \refqualclass{framework/qual}{Covariant}. @@ -464,7 +465,7 @@ The \<@Covariant> annotation is trusted but not checked. If you incorrectly specify as covariant a type parameter that can be -written (say, the class performs a +written (say, the class supports a \ operation or some other mutation on an object of that type), then you have created an unsoundness in the type system. For example, it would be incorrect to annotate the type parameter of @@ -950,6 +951,41 @@ qualifier on the upper bound). That is, the qualifier on \ is that same as that on \. +\subsectionAndLabel{Local variable defaults for types with qualifier parameters}{local-vars-qual-param-defaults} + +According to the CLIMB-to-top rule, local variables default to the top type (see +Section~\ref{climb-to-top}). Type refinement determines if a variable can be +treated as a suitable subtype, and annotations on local variables are rarely +needed as a result. However, since qualifier parameters add invariant subtyping, +type refinement is no longer valid. For example, suppose in the following code +that \ is annotated with \<@HasQualifierParameter(Tainted.class)>. + +\begin{Verbatim} + void method(@Untainted StringBuffer buffer) { + StringBuffer local = buffer; + executeSql(local.toString()); + } + + void executeSql(@Untainted String code) { + // ... + } +\end{Verbatim} + +Normally, the framework would determine that \ has type \<@Untainted +StringBuffer> and the call to \ would be valid. However, since by +default \ has type \<@Tainted StringBuffer>, and +\<@Untainted StringBuffer> is not a subtype, no type refinement would be +performed, leading to an error. Fixing this would require manually annotating +\ as an \<@Untainted StringBuffer>, increasing the annotation burden on +programmers. + +For this reason, local variables with types that have a qualifier parameter use +different defaulting rules. When a local variable has an initializer, the type +of that initializer is used as the default type of that variable if no other +annotations are written. For example, in the above code, the type of \ +would be \<@Untainted StringBuffer>. This eliminates the need for type +refinement. + \subsectionAndLabel{Qualifier parameters by default}{default-has-qualifier-parameter} If many classes in a project should have \<@HasQualifierParameter>, it's @@ -1002,7 +1038,7 @@ Types with qualifier parameters are only allowed as type arguments to type parameters whose upper bound have a qualifier parameter. If they were allowed for as type arguments for any type parameter, then -unsound casts would be permitted.For example: +unsound casts would be permitted. For example: \begin{Verbatim} @HasQualifierParameter(Tainted.class) diff --git a/docs/manual/inference.tex b/docs/manual/inference.tex index 3494b0ba557..c67c95c49e9 100644 --- a/docs/manual/inference.tex +++ b/docs/manual/inference.tex @@ -108,8 +108,14 @@ \subsubsectionAndLabel{Type inference for any type system}{type-inference-tools-general} -By supplying the \<-Ainfer=\emph{outputformat}> command-line option, -any type-checker can infer annotations. See Section~\ref{whole-program-inference}. +``Whole program inference'', or WPI, utilizes the \<-Ainfer=\emph{outputformat}> +command-line option. See Section~\ref{whole-program-inference}. + +\href{https://github.com/typetools/checker-framework-inference}{``Checker + Framework Inference''}, or CFI, is a type inference framework built on +the Checker Framework. You need to slightly rewrite your type system to +work with CFI\@. The CFI repository contains rewritten versions of some of +the type systems that are distributed with the Checker Framework. \href{https://github.com/reprogrammer/cascade/}{Cascade}~\cite{VakilianPEJ2014} is an Eclipse plugin that implements interactive type qualifier inference. diff --git a/docs/manual/interning-checker.tex b/docs/manual/interning-checker.tex index 2c708846de5..580bfcad7ad 100644 --- a/docs/manual/interning-checker.tex +++ b/docs/manual/interning-checker.tex @@ -128,16 +128,38 @@ \item[\refqualclass{checker/interning/qual}{UsesObjectEquals}] is a class annotation (not a type annotation) that indicates that this class's - \ method is the same as that of \. In other words, - neither this class nor any of its superclasses overrides the \ - method. Since \ uses reference equality, this means that - for such a class, \<==> and \ are equivalent, and so the - Interning Checker does not issue errors or warnings for either one. + \ method is the same as that of \. Since + \ uses reference equality, this means that for such a + class, \<==> and \ are equivalent, and so the Interning Checker + does not issue errors or warnings for either one. + + Two ways to satisfy this annotation are: (1) neither this class nor any + of its superclasses overrides the \ method, or (2) this class + defines \ with body \. \item[\refqualclass{checker/interning/qual}{InternMethod}] is a method declaration annotation that indicates that this method returns an interned object and may be invoked on an uninterned object. See Section~\ref{interning-intern-methods} for more details. + +\item[\refqualclass{checker/interning/qual}{EqualsMethod}] + is a method declaration annotation that indicates that this method + has a specification like \. The Interning Checker permits use + of \ within the body. + +\item[\refqualclass{checker/interning/qual}{CompareToMethod}] + is a method declaration annotation that indicates that this method + has a specification like \. The Interning Checker permits use + of \ within the body. + +\item[\refqualclass{checker/interning/qual}{FindDistinct}] + is a formal parameter declaration annotation that indicates that this + method uses \<==> to perform comparisons against the annotated formal + parameter. A common reason is that the method searches for the formal + parameter in some data structure, using \<==>. Within the method body, + the formal parameter's type is treated as + \refqualclass{checker/interning/qual}{InternedDistinct}, but any value + may be passed to the method. \end{description} \sectionAndLabel{Annotating your code with \code{@Interned}}{interning-annotating} diff --git a/docs/manual/introduction.tex b/docs/manual/introduction.tex index ca41f7bc1ca..76f8f837ed3 100644 --- a/docs/manual/introduction.tex +++ b/docs/manual/introduction.tex @@ -193,7 +193,7 @@ %BEGIN LATEX \\ %END LATEX - \url{https://checkerframework.org/checker-framework-3.5.0.zip} + \url{https://checkerframework.org/checker-framework-3.6.0.zip} \item Unzip it to create a \code{checker-framework-\ReleaseVersion{}} directory. @@ -635,7 +635,8 @@ to differ, just as Java does. See Section~\ref{covariant-type-parameters} and Section~\ref{invariant-arrays}. \item \<-AuseConservativeDefaultsForUncheckedCode> - Enables/disables conservative defaults in unchecked code. Takes arguments ``source,bytecode''. + Enables conservative defaults, and suppresses all type-checking warnings, + in unchecked code. Takes arguments ``source,bytecode''. ``-source,-bytecode'' is the (unsound) default setting. \begin{itemize} \item @@ -745,6 +746,7 @@ \<-AprintAllQualifiers>, \<-AprintVerboseGenerics>, \<-Anomsgtext> + \<-AdumpOnErrors> Amount of detail in messages; see Section~\ref{creating-debugging-options-detail}. \item @@ -787,6 +789,10 @@ \<-AatfCacheSize> Miscellaneous debugging options; see Section~\ref{creating-debugging-options-misc}. +\item + \<-Aversion> +Print the Checker Framework version. + \item \<-AprintGitProperties> Print information about the git repository from which the Checker Framework @@ -1091,7 +1097,7 @@ may need to be annotated \<@Nullable>. The specification should state any facts that are relevant to callees. -When checking a method, uses only the specification, not the +When checking a method, use only the specification, not the implementation, of other methods. (Equivalently, type-checking is ``modular'' or ``intraprocedural''.) @@ -1214,6 +1220,7 @@ \begin{Verbatim} java.lang.Double.valueOf(String) java.lang.String.contains(CharSequence) + java.lang.Object.checkNotNull(Object) org.junit.Assert.assertNotNull(Object) com.google.common.base.Preconditions.checkNotNull(Object) \end{Verbatim} diff --git a/docs/manual/manual.tex b/docs/manual/manual.tex index 858440bde49..27f861d60e8 100644 --- a/docs/manual/manual.tex +++ b/docs/manual/manual.tex @@ -4,8 +4,8 @@ \title{The Checker Framework Manual: \\ Custom pluggable types for Java} \author{\url{https://checkerframework.org/}} -\newcommand{\ReleaseVersion}{3.5.0} -\newcommand{\ReleaseInfo}{3.5.0 (1 Jul 2020)} +\newcommand{\ReleaseVersion}{3.6.0} +\newcommand{\ReleaseInfo}{3.6.0 (3 Aug 2020)} \date{Version \ReleaseInfo{}} \begin{document} diff --git a/docs/manual/signedness-checker.tex b/docs/manual/signedness-checker.tex index 8c46f0aacdc..cd319b70326 100644 --- a/docs/manual/signedness-checker.tex +++ b/docs/manual/signedness-checker.tex @@ -13,12 +13,15 @@ The range of signed byte values is -128 to 127. The range of unsigned byte values is 0 to 255. -Signedness is only applicable to integral types: \, \, -\, \, and \. Floating-point types (\ and -\) do not have operations that interpret the bits as unsigned. +Signedness is only applicable to the integral types \, +\, \, and \ and their boxed variants \, +\, \, and \. +\ and \ are always unsigned. +Floating-point types \, \, \, and \ are always signed. +% , because they do not have operations that interpret the bits as unsigned. Signedness is primarily about how the bits of the representation are -interpreted, not about the values that it can represent. An unsigned value +\emph{interpreted}, not about the values that it can represent. An unsigned value is always positive, but just because a variable's value is positive does not mean that it should be marked as \<@Unsigned>. If variable $v$ will be compared to a signed value, or used in arithmetic operations with a signed @@ -61,7 +64,8 @@ \item[\refqualclass{checker/signedness/qual}{SignedPositive}] indicates that a value is known at compile time to be in the positive signed range, so it has the same interpretation as signed or unsigned - and may be used with either interpretation. Programmers should usually + and may be used with either interpretation. (Equivalently, the most + significant bit is guaranteed to be 0.) Programmers should usually write \refqualclass{checker/signedness/qual}{Signed} or \refqualclass{checker/signedness/qual}{Unsigned} instead. @@ -112,6 +116,23 @@ \end{itemize} +\subsectionAndLabel{Widening conversions}{signedness-checker-widening-conversions} + +Figure~\ref{fig-signedness-hierarchy} shows the type qualifier hierarchy. +When upcasting among integral types, the expression has type +\<@SignedPositive> because the value is guaranteed to be within the signed +positive range. For example: + +\begin{Verbatim} +@Signed int sint; +@Unsigned int uint; +@SignedPositive long splong1 = sint; // legal +@SignedPositive long splong2 = uint; // legal +... (long) sint ... // this expression has type @SignedPositive long +... (long) uint ... // this expression has type @SignedPositive long +\end{Verbatim} + + \sectionAndLabel{Prohibited operations}{signedness-checker-prohibited-operations} The Signedness Checker prohibits the following uses of operators: diff --git a/docs/manual/troubleshooting.tex b/docs/manual/troubleshooting.tex index fd3d75baa32..ed0455045e4 100644 --- a/docs/manual/troubleshooting.tex +++ b/docs/manual/troubleshooting.tex @@ -100,6 +100,10 @@ \begin{Verbatim} .../bin/javac: Command not found \end{Verbatim} + +\begin{Verbatim} +error: Annotation processor 'org.checkerframework.checker.signedness.SignednessChecker' not found +\end{Verbatim} %BEGIN LATEX \end{smaller} %END LATEX @@ -126,7 +130,6 @@ described in Section~\ref{javac-wrapper}. \end{sloppypar} - \item If you get the error @@ -139,7 +142,6 @@ then an annotation is not present at run time that was present at compile time. For example, maybe when you compiled the code, the \<@Nullable> annotation was available, but it was not available at run time. -You can use JDK 8 at run time. \item If you get an error that contains lines like these: @@ -164,10 +166,12 @@ If you get an error such as \begin{Verbatim} -error: SourceChecker.typeProcess: unexpected Throwable (OutOfMemoryError) while processing ...; message: GC overhead limit exceeded +error: SourceChecker.typeProcess: unexpected Throwable (OutOfMemoryError) while processing ... + ; message: GC overhead limit exceeded \end{Verbatim} \noindent +(all on one line), then either give the JVM more memory when running the Checker Framework, or split your files and methods into smaller ones, or both. diff --git a/docs/tutorial/src/personalblog-demo/build.xml b/docs/tutorial/src/personalblog-demo/build.xml index 588563c2d75..92c7fbffa34 100644 --- a/docs/tutorial/src/personalblog-demo/build.xml +++ b/docs/tutorial/src/personalblog-demo/build.xml @@ -1,11 +1,11 @@ - + + - - + diff --git a/docs/tutorial/tests/testdemo/check-tainting.0.expected b/docs/tutorial/tests/testdemo/check-tainting.0.expected index d9c1784c9fa..6a0b0591626 100644 --- a/docs/tutorial/tests/testdemo/check-tainting.0.expected +++ b/docs/tutorial/tests/testdemo/check-tainting.0.expected @@ -5,7 +5,7 @@ Deleting directory /Users/smillst/src/jsr308/checker-framework/tutorial/eclipse- check-tainting: Created dir: /Users/smillst/src/jsr308/checker-framework/tutorial/eclipse-projects/personalblog-demo/bin Compiling 2 source files to /Users/smillst/src/jsr308/checker-framework/tutorial/eclipse-projects/personalblog-demo/bin -javac 1.8.0-jsr308-3.5.0 +javac 1.8.0-jsr308-3.6.0 /home/mernst/research/types/checker-framework/tutorial/eclipse-projects/personalblog-demo/src/net/eyde/personalblog/service/PersonalBlogService.java:174: error: [argument.type.incompatible] incompatible types in argument. + "%' order by post.created desc"); ^ diff --git a/docs/tutorial/webpages/security-error-cmd.html b/docs/tutorial/webpages/security-error-cmd.html index ba4ee3c0065..81941f06f8c 100644 --- a/docs/tutorial/webpages/security-error-cmd.html +++ b/docs/tutorial/webpages/security-error-cmd.html @@ -104,7 +104,7 @@

        1. Run the Tainting Checker — 1 error found

        check-tainting: [mkdir] Created dir: .../personalblog-demo/bin [jsr308.javac] Compiling 2 source files to .../personalblog-demo/bin -[jsr308.javac] javac 1.8.0-jsr308-3.5.0 +[jsr308.javac] javac 1.8.0-jsr308-3.6.0 [jsr308.javac] .../personalblog-demo/src/net/eyde/personalblog/service/PersonalBlogService.java:175: error: incompatible types in argument. [jsr308.javac] "where post.category like '%", category, [jsr308.javac] ^ @@ -153,7 +153,7 @@

        3. Re-run the Tainting Checker — a new error is found

        check-tainting: [mkdir] Created dir: .../personalblog-demo/bin [jsr308.javac] Compiling 2 source files to .../personalblog-demo/bin -[jsr308.javac] javac 1.8.0-jsr308-3.5.0 +[jsr308.javac] javac 1.8.0-jsr308-3.6.0 [jsr308.javac] .../personalblog-demo/src/net/eyde/personalblog/struts/action/ReadAction.java:58: error: incompatible types in argument. [jsr308.javac] pblog.getPostsByCategory(reqCategory)); [jsr308.javac] ^ @@ -196,7 +196,7 @@

        5. Re-run the Tainting Checker — no errors

        check-tainting: [mkdir] Created dir: .../personalblog-demo/bin [jsr308.javac] Compiling 2 source files to .../personalblog-demo/bin -[jsr308.javac] javac 1.8.0-jsr308-3.5.0 +[jsr308.javac] javac 1.8.0-jsr308-3.6.0 BUILD SUCCESSFUL Total time: 2 seconds diff --git a/framework-test/build.gradle b/framework-test/build.gradle index b9dc7b2705c..c10be8e1f0f 100644 --- a/framework-test/build.gradle +++ b/framework-test/build.gradle @@ -10,6 +10,7 @@ dependencies { // docs/developer/release/maven-artifacts/framework-test-pom.xml must be changed, too. implementation group: 'junit', name: 'junit', version: '4.13' implementation project(':javacutil') + implementation project(':checker-qual') if (Jvm.current().toolsJar) { tagletImplementation files(Jvm.current().toolsJar) diff --git a/framework-test/src/main/java/org/checkerframework/framework/test/FrameworkPerDirectoryTest.java b/framework-test/src/main/java/org/checkerframework/framework/test/FrameworkPerDirectoryTest.java deleted file mode 100644 index bc6259a51e0..00000000000 --- a/framework-test/src/main/java/org/checkerframework/framework/test/FrameworkPerDirectoryTest.java +++ /dev/null @@ -1,34 +0,0 @@ -package org.checkerframework.framework.test; - -import java.io.File; -import java.util.List; -import javax.annotation.processing.AbstractProcessor; - -/** - * The same as {@link org.checkerframework.framework.test.CheckerFrameworkPerDirectoryTest} except - * the annotated JDK is not used (because the {@code annotated-jdk} resource is under {@code - * checker/}, not {@code framework/}). - */ -public abstract class FrameworkPerDirectoryTest extends CheckerFrameworkPerDirectoryTest { - - /** - * Creates a new framework test. - * - *

        {@link TestConfigurationBuilder#getDefaultConfigurationBuilder(String, File, String, - * Iterable, Iterable, List, boolean)} adds additional checker options. - * - *

        These tests do not use the annotated JDK. - * - * @param testFiles the files containing test code, which will be type-checked - * @param checker the class for the checker to use - * @param testDir the path to the directory of test inputs - * @param checkerOptions options to pass to the compiler when running tests - */ - protected FrameworkPerDirectoryTest( - List testFiles, - Class checker, - String testDir, - String... checkerOptions) { - super(testFiles, checker, testDir, checkerOptions); - } -} diff --git a/framework-test/src/main/java/org/checkerframework/framework/test/FrameworkPerFileTest.java b/framework-test/src/main/java/org/checkerframework/framework/test/FrameworkPerFileTest.java deleted file mode 100644 index 777e2ec8ce1..00000000000 --- a/framework-test/src/main/java/org/checkerframework/framework/test/FrameworkPerFileTest.java +++ /dev/null @@ -1,33 +0,0 @@ -package org.checkerframework.framework.test; - -import java.io.File; -import java.util.List; -import javax.annotation.processing.AbstractProcessor; - -/** - * The same as {@link org.checkerframework.framework.test.CheckerFrameworkPerFileTest}, but does not - * use the annotated JDK (because the {@code annotated-jdk} resource is under {@code checker/}, not - * {@code framework/}). - */ -public abstract class FrameworkPerFileTest extends CheckerFrameworkPerFileTest { - /** - * Creates a new framework test. - * - *

        {@link TestConfigurationBuilder#getDefaultConfigurationBuilder(String, File, String, - * Iterable, Iterable, List, boolean)} adds additional checker options. - * - *

        These tests do not use the annotated JDK. - * - * @param testFile the file containing test code, which will be type-checked - * @param checker the class for the checker to use - * @param testDir the path to the directory of test inputs - * @param checkerOptions options to pass to the compiler when running tests - */ - protected FrameworkPerFileTest( - File testFile, - Class checker, - String testDir, - String... checkerOptions) { - super(testFile, checker, testDir, checkerOptions); - } -} diff --git a/framework-test/src/main/java/org/checkerframework/framework/test/ImmutableTestConfiguration.java b/framework-test/src/main/java/org/checkerframework/framework/test/ImmutableTestConfiguration.java index 09213aa7ff7..92f94d79e12 100644 --- a/framework-test/src/main/java/org/checkerframework/framework/test/ImmutableTestConfiguration.java +++ b/framework-test/src/main/java/org/checkerframework/framework/test/ImmutableTestConfiguration.java @@ -6,6 +6,7 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.javacutil.SystemUtil; /** @@ -25,7 +26,7 @@ public class ImmutableTestConfiguration implements TestConfiguration { * ) * } */ - private final Map options; + private final Map options; /** * These files contain diagnostics that should be returned by Javac. If this list is empty, the * diagnostics are instead read from comments in the Java file itself @@ -48,12 +49,13 @@ public ImmutableTestConfiguration( List diagnosticFiles, List testSourceFiles, List processors, - Map options, + Map options, boolean shouldEmitDebugInfo) { this.diagnosticFiles = Collections.unmodifiableList(diagnosticFiles); this.testSourceFiles = Collections.unmodifiableList(new ArrayList<>(testSourceFiles)); this.processors = Collections.unmodifiableList(new ArrayList<>(processors)); - this.options = Collections.unmodifiableMap(new LinkedHashMap<>(options)); + this.options = + Collections.unmodifiableMap(new LinkedHashMap(options)); this.shouldEmitDebugInfo = shouldEmitDebugInfo; } @@ -73,7 +75,7 @@ public List getProcessors() { } @Override - public Map getOptions() { + public Map getOptions() { return options; } diff --git a/framework-test/src/main/java/org/checkerframework/framework/test/PerDirectorySuite.java b/framework-test/src/main/java/org/checkerframework/framework/test/PerDirectorySuite.java index 4132ca19056..f0c6fe5ecab 100644 --- a/framework-test/src/main/java/org/checkerframework/framework/test/PerDirectorySuite.java +++ b/framework-test/src/main/java/org/checkerframework/framework/test/PerDirectorySuite.java @@ -51,6 +51,7 @@ protected List getChildren() { * * @param klass the class whose tests to run */ + @SuppressWarnings("nullness") // JUnit needs to be annotated public PerDirectorySuite(Class klass) throws Throwable { super(klass, Collections.emptyList()); final TestClass testClass = getTestClass(); @@ -63,6 +64,7 @@ public PerDirectorySuite(Class klass) throws Throwable { } /** Returns a list of one-element arrays, each containing a Java File. */ + @SuppressWarnings("nullness") // JUnit needs to be annotated private List> getParametersList(TestClass klass) throws Throwable { FrameworkMethod method = getParametersMethod(klass); @@ -158,6 +160,9 @@ public Object createTest() throws Exception { String testCaseName() { File file = javaFiles.get(0).getParentFile(); + if (file == null) { + throw new Error("root was passed? " + javaFiles.get(0)); + } return file.getPath().replace("tests" + System.getProperty("file.separator"), ""); } diff --git a/framework-test/src/main/java/org/checkerframework/framework/test/PerFileSuite.java b/framework-test/src/main/java/org/checkerframework/framework/test/PerFileSuite.java index 41e5961a0e3..5115b661740 100644 --- a/framework-test/src/main/java/org/checkerframework/framework/test/PerFileSuite.java +++ b/framework-test/src/main/java/org/checkerframework/framework/test/PerFileSuite.java @@ -51,6 +51,7 @@ protected List getChildren() { * * @param klass the class whose tests to run */ + @SuppressWarnings("nullness") // JUnit needs to be annotated public PerFileSuite(Class klass) throws Throwable { super(klass, Collections.emptyList()); final TestClass testClass = getTestClass(); @@ -63,7 +64,10 @@ public PerFileSuite(Class klass) throws Throwable { } /** Returns a list of one-element arrays, each containing a Java File. */ - @SuppressWarnings("unchecked") + @SuppressWarnings({ + "unchecked", + "nullness" // JUnit needs to be annotated + }) private List getParametersList(TestClass klass) throws Throwable { FrameworkMethod method = getParametersMethod(klass); @@ -126,9 +130,9 @@ private FrameworkMethod getParametersMethod(TestClass testClass) { break; case "getTestFiles": - // we'll force people to return a List for now but enforcing exactl List or a - // subtype thereof is not easy - if (!returnType.getCanonicalName().equals(List.class.getCanonicalName())) { + // We'll force people to return a List for now but enforcing exactly List or a + // subtype thereof is not easy. + if (!List.class.getCanonicalName().equals(returnType.getCanonicalName())) { throw new RuntimeException( "getTestFiles must return a List, found " + returnType); } diff --git a/framework-test/src/main/java/org/checkerframework/framework/test/SimpleOptionMap.java b/framework-test/src/main/java/org/checkerframework/framework/test/SimpleOptionMap.java index ae19edb0890..0f88b4aae6a 100644 --- a/framework-test/src/main/java/org/checkerframework/framework/test/SimpleOptionMap.java +++ b/framework-test/src/main/java/org/checkerframework/framework/test/SimpleOptionMap.java @@ -5,6 +5,7 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import org.checkerframework.checker.nullness.qual.Nullable; /** * SimpleOptionMap is a very basic Option container. The keys of the Option container are the set of @@ -24,14 +25,14 @@ */ public class SimpleOptionMap { /** A Map from optionName to arg, where arg is null if the option doesn't require any args. */ - private final Map options = new LinkedHashMap<>(); + private final Map options = new LinkedHashMap<>(); /** * Clears the current set of options and copies the input options to this map. * * @param options the new options to use for this object */ - public void setOptions(Map options) { + public void setOptions(Map options) { this.options.clear(); this.options.putAll(options); } @@ -104,7 +105,7 @@ public void addOptionIfValueNonEmpty(String option, String value) { * * @param options the options to add to this object */ - public void addOptions(Map options) { + public void addOptions(Map options) { this.options.putAll(options); } @@ -133,7 +134,7 @@ public void addOptions(Iterable newOptions) { * * @return the options in this object */ - public Map getOptions() { + public Map getOptions() { return options; } diff --git a/framework-test/src/main/java/org/checkerframework/framework/test/TestConfiguration.java b/framework-test/src/main/java/org/checkerframework/framework/test/TestConfiguration.java index 228aa7ce0b0..063eef626e0 100644 --- a/framework-test/src/main/java/org/checkerframework/framework/test/TestConfiguration.java +++ b/framework-test/src/main/java/org/checkerframework/framework/test/TestConfiguration.java @@ -3,6 +3,7 @@ import java.io.File; import java.util.List; import java.util.Map; +import org.checkerframework.checker.nullness.qual.Nullable; /** A configuration for running CheckerFrameworkTests or running the TypecheckExecutor. */ public interface TestConfiguration { @@ -60,7 +61,7 @@ public interface TestConfiguration { * @return a Map representing all command-line options to Javac other than source files and * processors */ - Map getOptions(); + Map getOptions(); /** * Returns the map returned by {@link #getOptions}, flattened into a list. The entries will be diff --git a/framework-test/src/main/java/org/checkerframework/framework/test/TestConfigurationBuilder.java b/framework-test/src/main/java/org/checkerframework/framework/test/TestConfigurationBuilder.java index 899251c3ce4..ea98cd51a8a 100644 --- a/framework-test/src/main/java/org/checkerframework/framework/test/TestConfigurationBuilder.java +++ b/framework-test/src/main/java/org/checkerframework/framework/test/TestConfigurationBuilder.java @@ -9,6 +9,9 @@ import java.util.List; import java.util.Map; import java.util.Set; +import org.checkerframework.checker.initialization.qual.UnknownInitialization; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.checker.nullness.qual.RequiresNonNull; import org.checkerframework.javacutil.BugInCF; import org.checkerframework.javacutil.SystemUtil; @@ -242,7 +245,7 @@ public List validate(boolean requireProcessors) { errors.add("No processors were specified!"); } - final Map optionMap = options.getOptions(); + final Map optionMap = options.getOptions(); if (!optionMap.containsKey("-d") || optionMap.get("-d") == null) { errors.add("No output directory was specified."); } @@ -289,7 +292,7 @@ public TestConfigurationBuilder setSourceFiles(List sourceFiles) { return this; } - public TestConfigurationBuilder setOptions(Map options) { + public TestConfigurationBuilder setOptions(Map options) { this.options.setOptions(options); return this; } @@ -312,7 +315,11 @@ public TestConfigurationBuilder addOptionIfValueNonEmpty(String option, String v return this; } - public TestConfigurationBuilder addOptions(Map options) { + @SuppressWarnings("nullness:return.type.incompatible") // need @PolyInitialized annotation + @RequiresNonNull("this.options") + public TestConfigurationBuilder addOptions( + @UnknownInitialization(TestConfigurationBuilder.class) TestConfigurationBuilder this, + Map options) { this.options.addOptions(options); return this; } diff --git a/framework-test/src/main/java/org/checkerframework/framework/test/TestUtilities.java b/framework-test/src/main/java/org/checkerframework/framework/test/TestUtilities.java index ad88a0df44f..79b7e6b5aad 100644 --- a/framework-test/src/main/java/org/checkerframework/framework/test/TestUtilities.java +++ b/framework-test/src/main/java/org/checkerframework/framework/test/TestUtilities.java @@ -21,6 +21,8 @@ import javax.tools.JavaCompiler; import javax.tools.JavaFileObject; import javax.tools.ToolProvider; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.javacutil.SystemUtil; import org.junit.Assert; @@ -86,12 +88,14 @@ public static List> findJavaFilesPerDirectory(File parent, String... * @return a list of list of Java test files */ private static List> findJavaTestFilesInDirectory(File dir) { - assert dir.isDirectory(); List> fileGroupedByDirectory = new ArrayList<>(); List filesInDir = new ArrayList<>(); fileGroupedByDirectory.add(filesInDir); String[] dirContents = dir.list(); + if (dirContents == null) { + throw new Error("Not a directory: " + dir); + } Arrays.sort(dirContents); for (String fileName : dirContents) { File file = new File(dir, fileName); @@ -145,7 +149,8 @@ public static List deeplyEnclosedJavaTestFiles(File directory) { List javaFiles = new ArrayList<>(); - File[] in = directory.listFiles(); + @SuppressWarnings("nullness") // checked above that it's a directory + File @NonNull [] in = directory.listFiles(); Arrays.sort( in, new Comparator() { @@ -201,7 +206,7 @@ public static boolean isJavaTestFile(File file) { return true; } - public static String diagnosticToString( + public static @Nullable String diagnosticToString( final Diagnostic diagnostic, boolean usingAnomsgtxt) { String result = diagnostic.toString().trim(); @@ -274,10 +279,10 @@ public static File findComparisonFile(File testFile) { return comparisonFile; } - public static List optionMapToList(Map options) { + public static List optionMapToList(Map options) { List optionList = new ArrayList<>(options.size() * 2); - for (Map.Entry opt : options.entrySet()) { + for (Map.Entry opt : options.entrySet()) { optionList.add(opt.getKey()); if (opt.getValue() != null) { diff --git a/framework-test/src/main/java/org/checkerframework/framework/test/TypecheckExecutor.java b/framework-test/src/main/java/org/checkerframework/framework/test/TypecheckExecutor.java index 69f515a14b1..a17ba5a7f3b 100644 --- a/framework-test/src/main/java/org/checkerframework/framework/test/TypecheckExecutor.java +++ b/framework-test/src/main/java/org/checkerframework/framework/test/TypecheckExecutor.java @@ -29,7 +29,11 @@ public TypecheckResult runTest(TestConfiguration configuration) { * configuration, and return place the result in a CompilationResult */ public CompilationResult compile(TestConfiguration configuration) { - TestUtilities.ensureDirectoryExists(new File(configuration.getOptions().get("-d"))); + String dOption = configuration.getOptions().get("-d"); + if (dOption == null) { + throw new Error("-d not supplied"); + } + TestUtilities.ensureDirectoryExists(new File(dOption)); final StringWriter javacOutput = new StringWriter(); DiagnosticCollector diagnostics = new DiagnosticCollector<>(); diff --git a/framework-test/src/main/java/org/checkerframework/framework/test/diagnostics/DiagnosticKind.java b/framework-test/src/main/java/org/checkerframework/framework/test/diagnostics/DiagnosticKind.java index da2b1cd68bb..2e145ae5fc2 100644 --- a/framework-test/src/main/java/org/checkerframework/framework/test/diagnostics/DiagnosticKind.java +++ b/framework-test/src/main/java/org/checkerframework/framework/test/diagnostics/DiagnosticKind.java @@ -2,6 +2,7 @@ import java.util.LinkedHashMap; import java.util.Map; +import org.checkerframework.checker.nullness.qual.Nullable; /** Indicates what type of Error was, or expected to be, encountered during typechecking. */ public enum DiagnosticKind { @@ -27,7 +28,7 @@ public enum DiagnosticKind { /** * Convert a string as it would appear in error messages or source code into a DiagnosticKind */ - public static DiagnosticKind fromParseString(String parseStr) { + public static @Nullable DiagnosticKind fromParseString(String parseStr) { return stringToCategory.get(parseStr); } } diff --git a/framework-test/src/main/java/org/checkerframework/framework/test/diagnostics/JavaDiagnosticReader.java b/framework-test/src/main/java/org/checkerframework/framework/test/diagnostics/JavaDiagnosticReader.java index a673aea5eb4..9a9746bb74d 100644 --- a/framework-test/src/main/java/org/checkerframework/framework/test/diagnostics/JavaDiagnosticReader.java +++ b/framework-test/src/main/java/org/checkerframework/framework/test/diagnostics/JavaDiagnosticReader.java @@ -10,6 +10,11 @@ import java.util.List; import java.util.NoSuchElementException; import javax.tools.JavaFileObject; +import org.checkerframework.checker.index.qual.GTENegativeOne; +import org.checkerframework.checker.nullness.qual.EnsuresNonNull; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.checker.nullness.qual.RequiresNonNull; /** * A file can indicate expected javac diagnostics. There are two types of such files: Java source @@ -150,8 +155,9 @@ private interface DiagnosticCodec { /// End of static methods, start of per-instance state /// - private final File toRead; - private final JavaFileObject toReadFileObject; + // Exactly one of toRead and toReadFileObject is non-null + private final @Nullable File toRead; + private final @Nullable JavaFileObject toReadFileObject; private final DiagnosticCodec codec; private final String filename; @@ -159,34 +165,38 @@ private interface DiagnosticCodec { private boolean initialized = false; private boolean closed = false; - private LineNumberReader reader = null; + private @MonotonicNonNull LineNumberReader reader = null; - private String nextLine = null; - private int nextLineNumber = -1; + private @Nullable String nextLine = null; + private @GTENegativeOne int nextLineNumber = -1; private JavaDiagnosticReader(File toRead, DiagnosticCodec codec) { this.toRead = toRead; this.toReadFileObject = null; this.codec = codec; - this.filename = shortFileName(toRead.getAbsolutePath()); + this.filename = shortFileName(toRead.getName()); } private JavaDiagnosticReader(JavaFileObject toRead, DiagnosticCodec codec) { this.toRead = null; this.toReadFileObject = toRead; this.codec = codec; - this.filename = shortFileName(toRead.getName()); + this.filename = shortFileName(toReadFileObject.getName()); } - private String shortFileName(String name) { + private static String shortFileName(String name) { int index = name.lastIndexOf(File.separator); return name.substring(index + 1, name.length()); } + @SuppressWarnings( + "nullness:contracts.postcondition.not.satisfied") // if initialized, reader is non-null + @EnsuresNonNull("reader") private void init() throws IOException { if (!initialized && !closed) { initialized = true; + @SuppressWarnings("nullness") // either toRead or toReadFileObject is non-null Reader fileReader = (toRead != null) ? new FileReader(toRead) : toReadFileObject.openReader(true); reader = new LineNumberReader(fileReader); @@ -195,6 +205,9 @@ private void init() throws IOException { } @Override + @SuppressWarnings( + "nullness:contracts.postcondition.not.satisfied") // if closed, reader is non-null + @EnsuresNonNull("reader") public boolean hasNext() { if (closed) { return false; @@ -223,7 +236,9 @@ public TestDiagnosticLine next() { if (nextLine == null) { throw new NoSuchElementException(); } else if (closed) { - throw new RuntimeException("Reader has been closed: " + toRead.getAbsolutePath()); + throw new RuntimeException( + "Reader has been closed: " + + ((toRead != null) ? toRead.getAbsolutePath() : toReadFileObject)); } String current = nextLine; @@ -252,11 +267,13 @@ public TestDiagnosticLine next() { } } + @RequiresNonNull("reader") protected void advance() throws IOException { nextLine = reader.readLine(); nextLineNumber = reader.getLineNumber(); } + @RequiresNonNull("reader") public void close() { try { if (initialized) { diff --git a/framework-test/src/main/java/org/checkerframework/framework/test/diagnostics/TestDiagnostic.java b/framework-test/src/main/java/org/checkerframework/framework/test/diagnostics/TestDiagnostic.java index 3526c779c88..eae22d96271 100644 --- a/framework-test/src/main/java/org/checkerframework/framework/test/diagnostics/TestDiagnostic.java +++ b/framework-test/src/main/java/org/checkerframework/framework/test/diagnostics/TestDiagnostic.java @@ -1,5 +1,7 @@ package org.checkerframework.framework.test.diagnostics; +import org.checkerframework.checker.nullness.qual.Nullable; + /** * Represents an expected error/warning message in a Java test file or an error/warning reported by * the Javac compiler. See {@link JavaDiagnosticReader} and {@link TestDiagnosticLine}. @@ -89,7 +91,7 @@ public String asSourceString() { * @return true if this and otherObj are equal according to lineNumber, kind, and message */ @Override - public boolean equals(Object otherObj) { + public boolean equals(@Nullable Object otherObj) { if (otherObj == null || otherObj.getClass() != TestDiagnostic.class) { return false; } diff --git a/framework-test/src/main/java/org/checkerframework/framework/test/diagnostics/TestDiagnosticUtils.java b/framework-test/src/main/java/org/checkerframework/framework/test/diagnostics/TestDiagnosticUtils.java index 9663f4dd0f5..9f7f308ee52 100644 --- a/framework-test/src/main/java/org/checkerframework/framework/test/diagnostics/TestDiagnosticUtils.java +++ b/framework-test/src/main/java/org/checkerframework/framework/test/diagnostics/TestDiagnosticUtils.java @@ -11,6 +11,8 @@ import java.util.regex.Pattern; import javax.tools.Diagnostic; import javax.tools.JavaFileObject; +import org.checkerframework.checker.nullness.qual.EnsuresNonNullIf; +import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.javacutil.Pair; /** A set of utilities and factory methods useful for working with TestDiagnostics. */ @@ -105,11 +107,12 @@ static Pair dropParentheses(final String str) { return Pair.of(false, str); } + @SuppressWarnings("nullness") // TODO: regular expression group access protected static TestDiagnostic fromPatternMatching( Pattern diagnosticPattern, Pattern warningPattern, String filename, - Long lineNumber, + @Nullable Long lineNumber, String diagnosticString) { final DiagnosticKind kind; final String message; @@ -211,6 +214,9 @@ private static Pair parseCategoryString(String category category = category.substring(fixable.length()); } DiagnosticKind categoryEnum = DiagnosticKind.fromParseString(category); + if (categoryEnum == null) { + throw new Error("Unparseable category: " + category); + } return Pair.of(categoryEnum, isFixable); } @@ -245,7 +251,8 @@ public static String handleEndOfLineJavaDiagnostic(String originalLine) { } /** Return true if this line in a Java file continues an expected diagnostic. */ - public static boolean isJavaDiagnosticLineContinuation(String originalLine) { + @EnsuresNonNullIf(result = true, expression = "#1") + public static boolean isJavaDiagnosticLineContinuation(@Nullable String originalLine) { if (originalLine == null) { return false; } diff --git a/framework/build.gradle b/framework/build.gradle index 767590149e7..9e296c139d3 100644 --- a/framework/build.gradle +++ b/framework/build.gradle @@ -18,7 +18,7 @@ sourceSets { } def versions = [ - autoValue : "1.7.3", + autoValue : "1.7.4", lombok : "1.18.12", ] @@ -33,8 +33,8 @@ dependencies { exclude group: 'com.google.errorprone', module: 'javac' } implementation project(':checker-qual') + implementation 'org.plumelib:plume-util:1.1.5' implementation 'org.plumelib:reflection-util:0.2.2' - implementation 'org.plumelib:plume-util:1.1.4' // TODO: org.checkerframework.annotatedlib:guava:28.2-jre requires the below dependency. // Get the version number from the "compile dependencies" section of diff --git a/framework/src/main/java/org/checkerframework/common/accumulation/AccumulationAnnotatedTypeFactory.java b/framework/src/main/java/org/checkerframework/common/accumulation/AccumulationAnnotatedTypeFactory.java index 481d6684f02..463a9a280de 100644 --- a/framework/src/main/java/org/checkerframework/common/accumulation/AccumulationAnnotatedTypeFactory.java +++ b/framework/src/main/java/org/checkerframework/common/accumulation/AccumulationAnnotatedTypeFactory.java @@ -1,5 +1,10 @@ package org.checkerframework.common.accumulation; +import com.github.javaparser.ParseProblemException; +import com.github.javaparser.StaticJavaParser; +import com.github.javaparser.ast.expr.BinaryExpr; +import com.github.javaparser.ast.expr.Expression; +import com.github.javaparser.ast.expr.UnaryExpr; import com.sun.source.tree.ExpressionTree; import com.sun.source.tree.MethodInvocationTree; import java.lang.annotation.Annotation; @@ -7,8 +12,11 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.StringJoiner; import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.ExecutableElement; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.common.basetype.BaseAnnotatedTypeFactory; import org.checkerframework.common.basetype.BaseTypeChecker; import org.checkerframework.common.returnsreceiver.ReturnsReceiverAnnotatedTypeFactory; @@ -25,6 +33,7 @@ import org.checkerframework.javacutil.AnnotationUtils; import org.checkerframework.javacutil.BugInCF; import org.checkerframework.javacutil.TreeUtils; +import org.checkerframework.javacutil.UserError; /** * An annotated type factory for an accumulation checker. @@ -50,6 +59,12 @@ public abstract class AccumulationAnnotatedTypeFactory extends BaseAnnotatedType */ private final Class accumulator; + /** + * The predicate annotation for this accumulation analysis, or null if predicates are not + * supported. A predicate annotation must have a single element named "value" of type String. + */ + private final @MonotonicNonNull Class predicate; + /** * Create an annotated type factory for an accumulation checker. * @@ -58,38 +73,70 @@ public abstract class AccumulationAnnotatedTypeFactory extends BaseAnnotatedType * argument named "value" whose type is a String array. * @param bottom the bottom type in the hierarchy, which must be a subtype of {@code * accumulator}. The bottom type should be an annotation with no arguments. + * @param predicate the predicate annotation. Either null (if predicates are not supported), or + * an annotation with a single element named "value" whose type is a String. */ protected AccumulationAnnotatedTypeFactory( BaseTypeChecker checker, Class accumulator, - Class bottom) { + Class bottom, + @Nullable Class predicate) { super(checker); this.accumulator = accumulator; - // Check that the requirements of the accumulator are met. Method[] accDeclaredMethods = accumulator.getDeclaredMethods(); if (accDeclaredMethods.length != 1) { rejectMalformedAccumulator("have exactly one element"); } - Method value = accDeclaredMethods[0]; - if (value.getName() != "value") { // interned + + Method accValue = accDeclaredMethods[0]; + if (accValue.getName() != "value") { // interned rejectMalformedAccumulator("name its element \"value\""); } - if (!value.getReturnType().isInstance(new String[0])) { + if (!accValue.getReturnType().isInstance(new String[0])) { rejectMalformedAccumulator("have an element of type String[]"); } - if (((String[]) value.getDefaultValue()).length != 0) { + if (((String[]) accValue.getDefaultValue()).length != 0) { rejectMalformedAccumulator("have the empty String array {} as its default value"); } + this.predicate = predicate; + // If there is a predicate annotation, check that its requirements are met. + if (predicate != null) { + Method[] predDeclaredMethods = predicate.getDeclaredMethods(); + if (predDeclaredMethods.length != 1) { + rejectMalformedPredicate("have exactly one element"); + } + Method predValue = predDeclaredMethods[0]; + if (predValue.getName() != "value") { // interned + rejectMalformedPredicate("name its element \"value\""); + } + if (!predValue.getReturnType().isInstance("")) { + rejectMalformedPredicate("have an element of type String"); + } + } + this.bottom = AnnotationBuilder.fromClass(elements, bottom); this.top = createAccumulatorAnnotation(Collections.emptyList()); - // Every subclass must call postInit! This does not do so for subclasses. - if (this.getClass() == AccumulationAnnotatedTypeFactory.class) { - this.postInit(); - } + // Every subclass must call postInit! This does not do so. + } + + /** + * Create an annotated type factory for an accumulation checker. + * + * @param checker the checker + * @param accumulator the accumulator type in the hierarchy. Must be an annotation with a single + * argument named "value" whose type is a String array. + * @param bottom the bottom type in the hierarchy, which must be a subtype of {@code + * accumulator}. The bottom type should be an annotation with no arguments. + */ + protected AccumulationAnnotatedTypeFactory( + BaseTypeChecker checker, + Class accumulator, + Class bottom) { + this(checker, accumulator, bottom, null); } /** @@ -99,7 +146,32 @@ protected AccumulationAnnotatedTypeFactory( * replace $MISSING$: "The accumulator annotation Foo must $MISSING$." */ private void rejectMalformedAccumulator(String missing) { - throw new BugInCF("The accumulator annotation " + accumulator + " must " + missing + "."); + rejectMalformedAnno("accumulator", accumulator, missing); + } + + /** + * Common error message for malformed predicate annotation. + * + * @param missing what is missing from the predicate, suitable for use in this string to replace + * $MISSING$: "The predicate annotation Foo must $MISSING$." + */ + private void rejectMalformedPredicate(String missing) { + rejectMalformedAnno("predicate", predicate, missing); + } + + /** + * Common error message implementation. Call rejectMalformedAccumulator or + * rejectMalformedPredicate as appropriate, rather than this method directly. + * + * @param annoTypeName the display name for the type of malformed annotation, such as + * "accumulator" + * @param anno the malformed annotation + * @param missing what is missing from the annotation, suitable for use in this string to + * replace $MISSING$: "The accumulator annotation Foo must $MISSING$." + */ + private void rejectMalformedAnno( + String annoTypeName, Class anno, String missing) { + throw new BugInCF("The " + annoTypeName + " annotation " + anno + " must " + missing + "."); } /** @@ -248,6 +320,26 @@ public List getAccumulatedValues(AnnotationMirror anno) { * | * bottom * + * + * Predicate subtyping is defined as follows: + * + *

          + *
        • An accumulator is a subtype of a predicate if substitution from the accumulator to the + * predicate makes the predicate true. For example, {@code Acc(A)} is a subtype of {@code + * AccPred("A || B")}, because when A is replaced with {@code true} and B is replaced with + * {@code false}, the resulting boolean formula evaluates to true. + *
        • A predicate P is a subtype of an accumulator iff after converting the accumulator into + * a predicate representing the conjunction of its elements, P is a subtype of that + * predicate according to the rule for subtyping between two predicates defined below. + *
        • A predicate P is a subtype of another predicate Q iff P and Q are equal. An extension + * point ({@link #isPredicateSubtype(String, String)}) is provided to allow more complex + * subtyping behavior between predicates. (The "correct" subtyping rule is that P is a + * subtype of Q iff P implies Q. That rule would require an SMT solver in the general + * case, which is undesirable because it would require an external dependency. A user can + * override {@link #isPredicateSubtype(String, String)} if they require more precise + * subtyping; the check described here is overly conservative (and therefore sound), but + * not very precise.) + *
        */ protected class AccumulationQualifierHierarchy extends MultiGraphQualifierHierarchy { @@ -276,6 +368,20 @@ public AnnotationMirror greatestLowerBound( return bottom; } + // If either is a predicate, then both should be converted to predicates and and-ed. + if (isPredicate(a1) || isPredicate(a2)) { + String a1Pred = convertToPredicate(a1); + String a2Pred = convertToPredicate(a2); + // check for top + if (a1Pred.isEmpty()) { + return a2; + } else if (a2Pred.isEmpty()) { + return a1; + } else { + return createPredicateAnnotation("(" + a1Pred + ") && (" + a2Pred + ")"); + } + } + List a1Val = getAccumulatedValues(a1); List a2Val = getAccumulatedValues(a2); // Avoid creating new annotation objects in the common case. @@ -302,6 +408,20 @@ public AnnotationMirror leastUpperBound( return a1; } + // If either is a predicate, then both should be converted to predicates and or-ed. + if (isPredicate(a1) || isPredicate(a2)) { + String a1Pred = convertToPredicate(a1); + String a2Pred = convertToPredicate(a2); + // check for top + if (a1Pred.isEmpty()) { + return a1; + } else if (a2Pred.isEmpty()) { + return a2; + } else { + return createPredicateAnnotation("(" + a1Pred + ") || (" + a2Pred + ")"); + } + } + List a1Val = getAccumulatedValues(a1); List a2Val = getAccumulatedValues(a2); // Avoid creating new annotation objects in the common case. @@ -324,9 +444,180 @@ public boolean isSubtype(final AnnotationMirror subAnno, final AnnotationMirror return false; } + if (isPredicate(subAnno)) { + return isPredicateSubtype( + convertToPredicate(subAnno), convertToPredicate(superAnno)); + } else if (isPredicate(superAnno)) { + return evaluatePredicate(subAnno, convertToPredicate(superAnno)); + } + List subVal = getAccumulatedValues(subAnno); List superVal = getAccumulatedValues(superAnno); return subVal.containsAll(superVal); } } + + /** + * Extension point for subtyping behavior between predicates. This implementation conservatively + * returns true only if the predicates are equal, or if the prospective supertype (q) is + * equivalent to top (that is, the empty string). + * + * @param p a predicate + * @param q another predicate + * @return true if p is a subtype of q + */ + protected boolean isPredicateSubtype(String p, String q) { + return "".equals(q) || p.equals(q); + } + + /** + * Evaluates whether the accumulator annotation {@code subAnno} makes the predicate {@code pred} + * true. + * + * @param subAnno an accumulator annotation + * @param pred a predicate + * @return whether the accumulator annotation satisfies the predicate + */ + protected boolean evaluatePredicate(AnnotationMirror subAnno, String pred) { + if (!isAccumulatorAnnotation(subAnno)) { + throw new BugInCF( + "tried to evaluate a predicate using an annotation that wasn't an accumulator: " + + subAnno); + } + List trueVariables = getAccumulatedValues(subAnno); + return evaluatePredicate(trueVariables, pred); + } + + /** + * Checks that the given annotation either: + * + *
          + *
        • does not contain a predicate, or + *
        • contains a parse-able predicate + *
        + * + * Used by the visitor to throw "predicate.invalid" errors; thus must be package-private. + * + * @param anm any annotation supported by this checker + * @return null if there is nothing wrong with the annotation, or an error message indicating + * the problem if it has an invalid predicate + */ + /* package-private */ + @Nullable String isValidPredicate(AnnotationMirror anm) { + String pred = convertToPredicate(anm); + try { + evaluatePredicate(Collections.emptyList(), pred); + } catch (UserError ue) { + return ue.getLocalizedMessage(); + } + return null; + } + + /** + * Evaluates whether treating the variables in {@code trueVariables} as {@code true} literals + * (and all other names as {@code false} literals) makes the predicate {@code pred} evaluate to + * true. + * + * @param trueVariables a list of names that should be replaced with {@code true} + * @param pred a predicate + * @return whether the true variables satisfy the predicate + */ + protected boolean evaluatePredicate(List trueVariables, String pred) { + Expression expression; + try { + expression = StaticJavaParser.parseExpression(pred); + } catch (ParseProblemException p) { + throw new UserError("unparseable predicate: " + pred + ". Parse exception: " + p); + } + return evaluateBooleanExpression(expression, trueVariables); + } + + /** + * Evaluates a boolean expression, in JavaParser format, that contains only and, or, + * parentheses, logical complement, and boolean literal nodes. + * + * @param expression a JavaParser boolean expression + * @param trueVariables the names of the variables that should be considered "true"; all other + * literals are considered "false" + * @return the result of evaluating the expression + */ + private boolean evaluateBooleanExpression(Expression expression, List trueVariables) { + if (expression.isNameExpr()) { + return trueVariables.contains(expression.asNameExpr().getNameAsString()); + } else if (expression.isBinaryExpr()) { + if (expression.asBinaryExpr().getOperator() == BinaryExpr.Operator.OR) { + return evaluateBooleanExpression(expression.asBinaryExpr().getLeft(), trueVariables) + || evaluateBooleanExpression( + expression.asBinaryExpr().getRight(), trueVariables); + } else if (expression.asBinaryExpr().getOperator() == BinaryExpr.Operator.AND) { + return evaluateBooleanExpression(expression.asBinaryExpr().getLeft(), trueVariables) + && evaluateBooleanExpression( + expression.asBinaryExpr().getRight(), trueVariables); + } + } else if (expression.isEnclosedExpr()) { + return evaluateBooleanExpression(expression.asEnclosedExpr().getInner(), trueVariables); + } else if (expression.isUnaryExpr()) { + if (expression.asUnaryExpr().getOperator() == UnaryExpr.Operator.LOGICAL_COMPLEMENT) { + return !evaluateBooleanExpression( + expression.asUnaryExpr().getExpression(), trueVariables); + } + } + // This could be a BugInCF if there is a bug in the code above. + throw new UserError( + "encountered an unexpected type of expression in a " + + "predicate expression: " + + expression + + " was of type " + + expression.getClass()); + } + + /** + * Creates a new predicate annotation from the given string. + * + * @param p a valid predicate + * @return an annotation representing that predicate + */ + protected AnnotationMirror createPredicateAnnotation(String p) { + AnnotationBuilder builder = new AnnotationBuilder(processingEnv, predicate); + builder.setValue("value", p); + return builder.build(); + } + + /** + * Converts the given annotation mirror to a predicate. + * + * @param anno an annotation + * @return the predicate, as a String, that is equivalent to that annotation. May return the + * empty string. + */ + protected String convertToPredicate(AnnotationMirror anno) { + if (AnnotationUtils.areSame(anno, bottom)) { + return "false"; + } else if (isPredicate(anno)) { + if (AnnotationUtils.hasElementValue(anno, "value")) { + return AnnotationUtils.getElementValue(anno, "value", String.class, false); + } else { + return ""; + } + } else if (isAccumulatorAnnotation(anno)) { + List values = getAccumulatedValues(anno); + StringJoiner sj = new StringJoiner(" && "); + for (String value : values) { + sj.add(value); + } + return sj.toString(); + } else { + throw new BugInCF("annotation is not bottom, a predicate, or an accumulator: " + anno); + } + } + + /** + * Returns true if anno is a predicate annotation. + * + * @param anno an annotation + * @return true if anno is a predicate annotation + */ + protected boolean isPredicate(AnnotationMirror anno) { + return predicate != null && AnnotationUtils.areSameByClass(anno, predicate); + } } diff --git a/framework/src/main/java/org/checkerframework/common/accumulation/AccumulationTransfer.java b/framework/src/main/java/org/checkerframework/common/accumulation/AccumulationTransfer.java index 9ba36e587be..293c2184524 100644 --- a/framework/src/main/java/org/checkerframework/common/accumulation/AccumulationTransfer.java +++ b/framework/src/main/java/org/checkerframework/common/accumulation/AccumulationTransfer.java @@ -13,6 +13,7 @@ import org.checkerframework.dataflow.analysis.TransferResult; import org.checkerframework.dataflow.cfg.node.MethodInvocationNode; import org.checkerframework.dataflow.cfg.node.Node; +import org.checkerframework.framework.flow.CFAbstractStore; import org.checkerframework.framework.flow.CFAnalysis; import org.checkerframework.framework.flow.CFStore; import org.checkerframework.framework.flow.CFTransfer; @@ -75,18 +76,24 @@ public void accumulate(Node node, TransferResult result, Strin List valuesAsList = Arrays.asList(values); // If dataflow has already recorded information about the target, fetch it and integrate // it into the list of values in the new annotation. - CFValue flowValue = result.getResultValue(); - if (flowValue != null) { - Set flowAnnos = flowValue.getAnnotations(); - assert flowAnnos.size() <= 1; - for (AnnotationMirror anno : flowAnnos) { - List oldFlowValues = - ValueCheckerUtils.getValueOfAnnotationWithStringArgument(anno); - if (oldFlowValues != null) { - // valuesAsList cannot have its length changed -- it is backed by an array. - // getValueOfAnnotationWithStringArgument returns a new, modifiable list. - oldFlowValues.addAll(valuesAsList); - valuesAsList = oldFlowValues; + Receiver target = FlowExpressions.internalReprOf(typeFactory, node); + if (CFAbstractStore.canInsertReceiver(target)) { + CFValue flowValue = result.getRegularStore().getValue(target); + if (flowValue != null) { + Set flowAnnos = flowValue.getAnnotations(); + assert flowAnnos.size() <= 1; + for (AnnotationMirror anno : flowAnnos) { + if (typeFactory.isAccumulatorAnnotation(anno)) { + List oldFlowValues = + ValueCheckerUtils.getValueOfAnnotationWithStringArgument(anno); + if (oldFlowValues != null) { + // valuesAsList cannot have its length changed -- it is backed by an + // array. getValueOfAnnotationWithStringArgument returns a new, + // modifiable list. + oldFlowValues.addAll(valuesAsList); + valuesAsList = oldFlowValues; + } + } } } } diff --git a/framework/src/main/java/org/checkerframework/common/accumulation/AccumulationVisitor.java b/framework/src/main/java/org/checkerframework/common/accumulation/AccumulationVisitor.java new file mode 100644 index 00000000000..12bc8ba7e4d --- /dev/null +++ b/framework/src/main/java/org/checkerframework/common/accumulation/AccumulationVisitor.java @@ -0,0 +1,36 @@ +package org.checkerframework.common.accumulation; + +import com.sun.source.tree.AnnotationTree; +import javax.lang.model.element.AnnotationMirror; +import org.checkerframework.common.basetype.BaseTypeChecker; +import org.checkerframework.common.basetype.BaseTypeVisitor; +import org.checkerframework.javacutil.TreeUtils; + +/** + * The visitor for an accumulation checker. Issues predicate.invalid errors if the user writes an + * invalid predicate. + */ +public class AccumulationVisitor extends BaseTypeVisitor { + + /** + * Constructor matching super. + * + * @param checker the checker + */ + public AccumulationVisitor(BaseTypeChecker checker) { + super(checker); + } + + /** Checks each predicate annotation to make sure the predicate is well-formed. */ + @Override + public Void visitAnnotation(final AnnotationTree node, final Void p) { + AnnotationMirror anno = TreeUtils.annotationFromAnnotationTree(node); + if (atypeFactory.isPredicate(anno)) { + String errorMessage = atypeFactory.isValidPredicate(anno); + if (errorMessage != null) { + checker.reportError(node, "predicate.invalid", errorMessage); + } + } + return super.visitAnnotation(node, p); + } +} diff --git a/framework/src/main/java/org/checkerframework/common/accumulation/messages.properties b/framework/src/main/java/org/checkerframework/common/accumulation/messages.properties new file mode 100644 index 00000000000..9dab1ab673e --- /dev/null +++ b/framework/src/main/java/org/checkerframework/common/accumulation/messages.properties @@ -0,0 +1 @@ +predicate.invalid=Unparseable predicate. Predicates must be produced by this grammar: S --> method name | (S) | S && S | S || S. The message from the evaluator was: %s diff --git a/framework/src/main/java/org/checkerframework/common/aliasing/AliasingVisitor.java b/framework/src/main/java/org/checkerframework/common/aliasing/AliasingVisitor.java index 63d7ba5678a..32e9ee3a71a 100644 --- a/framework/src/main/java/org/checkerframework/common/aliasing/AliasingVisitor.java +++ b/framework/src/main/java/org/checkerframework/common/aliasing/AliasingVisitor.java @@ -10,6 +10,9 @@ import com.sun.source.tree.VariableTree; import com.sun.source.util.TreePath; 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.ExecutableElement; import javax.lang.model.element.VariableElement; import org.checkerframework.checker.compilermsgs.qual.CompilerMessageKey; @@ -22,6 +25,7 @@ import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedArrayType; import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedDeclaredType; import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedExecutableType; +import org.checkerframework.javacutil.AnnotationUtils; import org.checkerframework.javacutil.TreeUtils; /** @@ -150,8 +154,11 @@ private void isUniqueCheck( // this isn't called for pseudo-assignments. @Override protected void commonAssignmentCheck( - Tree varTree, ExpressionTree valueExp, @CompilerMessageKey String errorKey) { - super.commonAssignmentCheck(varTree, valueExp, errorKey); + Tree varTree, + ExpressionTree valueExp, + @CompilerMessageKey String errorKey, + Object... extraArgs) { + super.commonAssignmentCheck(varTree, valueExp, errorKey, extraArgs); if (isInUniqueConstructor() && TreeUtils.isExplicitThisDereference(valueExp)) { // If an assignment occurs inside a constructor with // result type @Unique, it will invalidate the @Unique property @@ -167,8 +174,9 @@ protected void commonAssignmentCheck( AnnotatedTypeMirror varType, AnnotatedTypeMirror valueType, Tree valueTree, - @CompilerMessageKey String errorKey) { - super.commonAssignmentCheck(varType, valueType, valueTree, errorKey); + @CompilerMessageKey String errorKey, + Object... extraArgs) { + super.commonAssignmentCheck(varType, valueType, valueTree, errorKey, extraArgs); // If we are visiting a pseudo-assignment, visitorLeafKind is either // Kind.NEW_CLASS or Kind.METHOD_INVOCATION. @@ -263,7 +271,9 @@ protected void checkThisOrSuperConstructorCall( /** * Returns true if {@code exp} has type {@code @Unique} and is not a method invocation nor a new - * class expression. + * class expression. It checks whether the tree expression is unique by either checking for an + * explicit annotation or checking whether the class of the tree expression {@code exp} has type + * {@code @Unique} * * @param exp the Tree to check */ @@ -271,7 +281,30 @@ private boolean canBeLeaked(Tree exp) { AnnotatedTypeMirror type = atypeFactory.getAnnotatedType(exp); boolean isMethodInvocation = exp.getKind() == Kind.METHOD_INVOCATION; boolean isNewClass = exp.getKind() == Kind.NEW_CLASS; - return type.hasExplicitAnnotation(Unique.class) && !isMethodInvocation && !isNewClass; + boolean isUniqueType = isUniqueClass(type) || type.hasExplicitAnnotation(Unique.class); + return isUniqueType && !isMethodInvocation && !isNewClass; + } + + /** + * Return true if the class declaration for annotated type {@code type} has annotation + * {@code @Unique}. + * + * @param type the annotated type whose class must be checked + * @return boolean true if class is unique and false otherwise + */ + private boolean isUniqueClass(AnnotatedTypeMirror type) { + Element el = types.asElement(type.getUnderlyingType()); + if (el == null) { + return false; + } + Set annoMirrors = atypeFactory.getDeclAnnotations(el); + if (annoMirrors == null) { + return false; + } + if (AnnotationUtils.containsSameByClass(annoMirrors, Unique.class)) { + return true; + } + return false; } private boolean isInUniqueConstructor() { diff --git a/framework/src/main/java/org/checkerframework/common/basetype/BaseTypeChecker.java b/framework/src/main/java/org/checkerframework/common/basetype/BaseTypeChecker.java index 78b20dd3dba..211106a3e1d 100644 --- a/framework/src/main/java/org/checkerframework/common/basetype/BaseTypeChecker.java +++ b/framework/src/main/java/org/checkerframework/common/basetype/BaseTypeChecker.java @@ -25,6 +25,8 @@ import javax.lang.model.element.Element; import javax.lang.model.element.TypeElement; import javax.tools.Diagnostic; +import org.checkerframework.checker.interning.qual.FindDistinct; +import org.checkerframework.checker.interning.qual.InternedDistinct; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.checker.signature.qual.ClassGetName; @@ -41,6 +43,7 @@ import org.checkerframework.javacutil.AnnotationProvider; import org.checkerframework.javacutil.BugInCF; import org.checkerframework.javacutil.InternalUtils; +import org.checkerframework.javacutil.TypeSystemError; import org.checkerframework.javacutil.UserError; /** @@ -298,10 +301,9 @@ public static T invokeConstructorFor( } catch (Throwable t) { if (t instanceof InvocationTargetException) { Throwable err = t.getCause(); - if (err instanceof UserError) { - UserError ue = (UserError) err; + if (err instanceof UserError || err instanceof TypeSystemError) { // Don't add another stack frame, just show the message. - throw ue; + throw (RuntimeException) err; } throw new BugInCF( String.format( @@ -561,14 +563,16 @@ protected void warnUnneededSuppressions() { * *

        Otherwise, it prints the message. */ + @SuppressWarnings("interning:not.interned") // assertion @Override protected void printOrStoreMessage( Diagnostic.Kind kind, String message, Tree source, CompilationUnitTree root) { assert this.currentRoot == root; + StackTraceElement[] trace = Thread.currentThread().getStackTrace(); if (messageStore == null) { - super.printOrStoreMessage(kind, message, source, root); + super.printOrStoreMessage(kind, message, source, root, trace); } else { - CheckerMessage checkerMessage = new CheckerMessage(kind, message, source, this); + CheckerMessage checkerMessage = new CheckerMessage(kind, message, source, this, trace); messageStore.add(checkerMessage); } } @@ -583,29 +587,48 @@ protected void printOrStoreMessage( private void printStoredMessages(CompilationUnitTree unit) { if (messageStore != null) { for (CheckerMessage msg : messageStore) { - super.printOrStoreMessage(msg.kind, msg.message, msg.source, unit); + super.printOrStoreMessage(msg.kind, msg.message, msg.source, unit, msg.trace); } } } /** Represents a message (e.g., an error message) issued by a checker. */ private static class CheckerMessage { + /** The severity of the message. */ final Diagnostic.Kind kind; + /** The message itself. */ final String message; - final Tree source; + /** The source code that the message is about. */ + final @InternedDistinct Tree source; + /** Stores the stack trace when the message is created. */ + final StackTraceElement[] trace; /** * The checker that issued this message. The compound checker that depends on this checker * uses this to sort the messages. */ - final BaseTypeChecker checker; + final @InternedDistinct BaseTypeChecker checker; + /** + * Create a new CheckerMessage. + * + * @param kind kind of diagnostic, for example, error or warning + * @param message error message that needs to be printed + * @param source tree element causing the error + * @param checker the type-checker in use + * @param trace the stack trace when the message is created + */ private CheckerMessage( - Diagnostic.Kind kind, String message, Tree source, BaseTypeChecker checker) { + Diagnostic.Kind kind, + String message, + @FindDistinct Tree source, + @FindDistinct BaseTypeChecker checker, + StackTraceElement[] trace) { this.kind = kind; this.message = message; this.source = source; this.checker = checker; + this.trace = trace; } @Override diff --git a/framework/src/main/java/org/checkerframework/common/basetype/BaseTypeValidator.java b/framework/src/main/java/org/checkerframework/common/basetype/BaseTypeValidator.java index f5a7e125d0c..135bd068a37 100644 --- a/framework/src/main/java/org/checkerframework/common/basetype/BaseTypeValidator.java +++ b/framework/src/main/java/org/checkerframework/common/basetype/BaseTypeValidator.java @@ -41,7 +41,7 @@ public class BaseTypeValidator extends AnnotatedTypeScanner implemen protected boolean isValid = true; /** Should the primary annotation on the top level type be checked? */ - protected boolean checkTopLevelDeclaredType = true; + protected boolean checkTopLevelDeclaredOrPrimitiveType = true; /** BaseTypeChecker. */ protected final BaseTypeChecker checker; @@ -80,22 +80,27 @@ public boolean isValid(AnnotatedTypeMirror type, Tree tree) { return false; } this.isValid = true; - this.checkTopLevelDeclaredType = shouldCheckTopLevelDeclaredType(type, tree); + this.checkTopLevelDeclaredOrPrimitiveType = + shouldCheckTopLevelDeclaredOrPrimitiveType(type, tree); visit(type, tree); return this.isValid; } /** - * Should the top-level declared type be checked? + * Should the top-level declared or primitive type be checked? + * + *

        If {@code type} is not a declared or primitive type, then this method returns true. * *

        Top-level type is not checked if tree is a local variable or an expression tree. * * @param type AnnotatedTypeMirror being validated * @param tree a Tree whose type is {@code type} - * @return whether or not the top-level type should be checked + * @return whether or not the top-level type should be checked, if {@code type} is a declared or + * primitive type. */ - protected boolean shouldCheckTopLevelDeclaredType(AnnotatedTypeMirror type, Tree tree) { - if (type.getKind() != TypeKind.DECLARED) { + protected boolean shouldCheckTopLevelDeclaredOrPrimitiveType( + AnnotatedTypeMirror type, Tree tree) { + if (type.getKind() != TypeKind.DECLARED && !type.getKind().isPrimitive()) { return true; } return !TreeUtils.isLocalVariable(tree) @@ -110,15 +115,15 @@ protected boolean shouldCheckTopLevelDeclaredType(AnnotatedTypeMirror type, Tree *

        Currently, the following is checked: * *

          - *
        1. There should not be multiple annotations from the same hierarchy. - *
        2. There should not be more annotations than the width of the qualifier hierarchy. + *
        3. There should not be multiple annotations from the same qualifier hierarchy. + *
        4. There should not be more annotations than the width of the QualifierHierarchy. *
        5. If the type is not a type variable, then the number of annotations should be the same - * as the width of the qualifier hierarchy. + * as the width of the QualifierHierarchy. *
        6. These properties should also hold recursively for component types of arrays, as wells * as bounds of type variables and wildcards. *
        * - * @param qualifierHierarchy the qualifier hierachy + * @param qualifierHierarchy the QualifierHierarchy * @param type the type to test * @return list of reasons the type is invalid, or empty list if the type is valid */ @@ -136,7 +141,7 @@ protected List isValidType( * Checks every property listed in {@link #isValidType}, but only for the top level type. If * successful, returns an empty list. If not successful, returns diagnostics. * - * @param qualifierHierarchy the qualifier hierarchy + * @param qualifierHierarchy the QualifierHierarchy * @param type the type to be checked * @return the diagnostics indicating failure, or an empty list if successful */ @@ -253,7 +258,7 @@ public Void visitDeclared(AnnotatedDeclaredType type, Tree tree) { final boolean skipChecks = checker.shouldSkipUses(type.getUnderlyingType().asElement()); - if (checkTopLevelDeclaredType && !skipChecks) { + if (checkTopLevelDeclaredOrPrimitiveType && !skipChecks) { // Ensure that type use is a subtype of the element type // isValidUse determines the erasure of the types. @@ -270,7 +275,7 @@ public Void visitDeclared(AnnotatedDeclaredType type, Tree tree) { } // Set checkTopLevelDeclaredType to true, because the next time visitDeclared is called, // the type isn't the top level, so always do the check. - checkTopLevelDeclaredType = true; + checkTopLevelDeclaredOrPrimitiveType = true; /* * Try to reconstruct the ParameterizedTypeTree from the given tree. @@ -403,7 +408,8 @@ private Pair extractParameterizedT @Override public Void visitPrimitive(AnnotatedPrimitiveType type, Tree tree) { - if (checker.shouldSkipUses(type.getUnderlyingType().toString())) { + if (!checkTopLevelDeclaredOrPrimitiveType + || checker.shouldSkipUses(type.getUnderlyingType().toString())) { return super.visitPrimitive(type, tree); } @@ -460,7 +466,13 @@ protected Void visitParameterizedType(AnnotatedDeclaredType type, ParameterizedT List bounds = atypeFactory.typeVariablesFromUse(type, element); - visitor.checkTypeArguments(tree, bounds, type.getTypeArguments(), tree.getTypeArguments()); + visitor.checkTypeArguments( + tree, + bounds, + type.getTypeArguments(), + tree.getTypeArguments(), + element.getSimpleName(), + element.getTypeParameters()); return null; } diff --git a/framework/src/main/java/org/checkerframework/common/basetype/BaseTypeVisitor.java b/framework/src/main/java/org/checkerframework/common/basetype/BaseTypeVisitor.java index 36b0a5fe848..1fb019a6bc4 100644 --- a/framework/src/main/java/org/checkerframework/common/basetype/BaseTypeVisitor.java +++ b/framework/src/main/java/org/checkerframework/common/basetype/BaseTypeVisitor.java @@ -47,11 +47,11 @@ import java.util.EnumSet; import java.util.HashMap; import java.util.HashSet; -import java.util.Iterator; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.StringJoiner; import javax.annotation.processing.ProcessingEnvironment; import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.AnnotationValue; @@ -68,6 +68,7 @@ import javax.lang.model.util.ElementFilter; import javax.tools.Diagnostic.Kind; import org.checkerframework.checker.compilermsgs.qual.CompilerMessageKey; +import org.checkerframework.checker.interning.qual.FindDistinct; import org.checkerframework.dataflow.analysis.FlowExpressions; import org.checkerframework.dataflow.analysis.FlowExpressions.Receiver; import org.checkerframework.dataflow.analysis.TransferResult; @@ -117,6 +118,7 @@ import org.checkerframework.javacutil.BugInCF; import org.checkerframework.javacutil.ElementUtils; import org.checkerframework.javacutil.Pair; +import org.checkerframework.javacutil.SystemUtil; import org.checkerframework.javacutil.TreeUtils; import org.checkerframework.javacutil.TypesUtils; @@ -744,11 +746,13 @@ public Void visitMethod(MethodTree node, Void p) { * @param node the method tree to check */ protected void checkPurity(MethodTree node) { + if (!checker.hasOption("checkPurityAnnotations")) { + return; + } + boolean anyPurityAnnotation = PurityUtils.hasPurityAnnotation(atypeFactory, node); boolean suggestPureMethods = checker.hasOption("suggestPureMethods"); - boolean checkPurityAnnotations = checker.hasOption("checkPurityAnnotations"); - - if (!checkPurityAnnotations || (!anyPurityAnnotation && !suggestPureMethods)) { + if (!anyPurityAnnotation && !suggestPureMethods) { return; } @@ -876,7 +880,7 @@ private void reportPurityError(String msgKeyPrefix, Pair r) { String reason = r.second; @SuppressWarnings("CompilerMessages") @CompilerMessageKey String msgKey = msgKeyPrefix + reason; - if (reason.equals("call")) { + if (reason.equals("call") || reason.equals("call.method")) { MethodInvocationTree mitree = (MethodInvocationTree) r.first; checker.reportError(r.first, msgKey, mitree.getMethodSelect()); } else { @@ -1280,7 +1284,7 @@ private boolean isTypeAnnotation(AnnotationTree anno) { /** * Performs two checks: subtyping and assignability checks, using {@link - * #commonAssignmentCheck(Tree, ExpressionTree, String)}. + * #commonAssignmentCheck(Tree, ExpressionTree, String, Object[])}. * *

        If the subtype check fails, it issues a "assignment.type.incompatible" error. */ @@ -1367,11 +1371,19 @@ public Void visitMethodInvocation(MethodInvocationTree node, Void p) { paramBounds.add(param.getBounds()); } - checkTypeArguments(node, paramBounds, typeargs, node.getTypeArguments()); + ExecutableElement method = invokedMethod.getElement(); + Name methodName = method.getSimpleName(); + checkTypeArguments( + node, + paramBounds, + typeargs, + node.getTypeArguments(), + methodName, + invokedMethod.getTypeVariables()); List params = AnnotatedTypes.expandVarArgs(atypeFactory, invokedMethod, node.getArguments()); - checkArguments(params, node.getArguments()); + checkArguments(params, node.getArguments(), methodName, method.getParameters()); checkVarargs(invokedMethod, node); if (ElementUtils.isMethod( @@ -1404,9 +1416,11 @@ public Void visitMethodInvocation(MethodInvocationTree node, Void p) { * Checks that the following rule is satisfied: The type on a constructor declaration must be a * supertype of the return type of "this()" invocation within that constructor. * - *

        Subclass can override this method to change the behavior for just "this" constructor - * class. Or {@link #checkThisOrSuperConstructorCall(MethodInvocationTree, String)} to change - * the behavior for "this" and "super" constructor calls. + *

        Subclasses can override this method to change the behavior for just "this" constructor + * class. Or override {@link #checkThisOrSuperConstructorCall(MethodInvocationTree, String)} to + * change the behavior for "this" and "super" constructor calls. + * + * @param thisCall the AST node for the constructor call */ protected void checkThisConstructorCall(MethodInvocationTree thisCall) { checkThisOrSuperConstructorCall(thisCall, "this.invocation.invalid"); @@ -1416,9 +1430,11 @@ protected void checkThisConstructorCall(MethodInvocationTree thisCall) { * Checks that the following rule is satisfied: The type on a constructor declaration must be a * supertype of the return type of "super()" invocation within that constructor. * - *

        Subclass can override this method to change the behavior for just "super" constructor - * class. Or {@link #checkThisOrSuperConstructorCall(MethodInvocationTree, String)} to change - * the behavior for "this" and "super" constructor calls. + *

        Subclasses can override this method to change the behavior for just "super" constructor + * class. Or override {@link #checkThisOrSuperConstructorCall(MethodInvocationTree, String)} to + * change the behavior for "this" and "super" constructor calls. + * + * @param superCall the AST node for the super constructor call */ protected void checkSuperConstructorCall(MethodInvocationTree superCall) { checkThisOrSuperConstructorCall(superCall, "super.invocation.invalid"); @@ -1427,12 +1443,15 @@ protected void checkSuperConstructorCall(MethodInvocationTree superCall) { /** * Checks that the following rule is satisfied: The type on a constructor declaration must be a * supertype of the return type of "this()" or "super()" invocation within that constructor. + * + * @param call the AST node for the constructor call + * @param errorKey the error message key to use if the check fails */ protected void checkThisOrSuperConstructorCall( - MethodInvocationTree superCall, @CompilerMessageKey String errorKey) { - TreePath path = atypeFactory.getPath(superCall); + MethodInvocationTree call, @CompilerMessageKey String errorKey) { + TreePath path = atypeFactory.getPath(call); MethodTree enclosingMethod = TreeUtils.enclosingMethod(path); - AnnotatedTypeMirror superType = atypeFactory.getAnnotatedType(superCall); + AnnotatedTypeMirror superType = atypeFactory.getAnnotatedType(call); AnnotatedExecutableType constructorType = atypeFactory.getAnnotatedType(enclosingMethod); Set topAnnotations = atypeFactory.getQualifierHierarchy().getTopAnnotations(); @@ -1444,8 +1463,7 @@ protected void checkThisOrSuperConstructorCall( if (!atypeFactory .getQualifierHierarchy() .isSubtype(superTypeMirror, constructorTypeMirror)) { - checker.reportError( - superCall, errorKey, constructorTypeMirror, superCall, superTypeMirror); + checker.reportError(call, errorKey, constructorTypeMirror, call, superTypeMirror); } } } @@ -1458,7 +1476,7 @@ protected void checkThisOrSuperConstructorCall( *

        Note it's required that type checking for each element in varargs is executed by the * caller before or after calling this method. * - * @see #checkArguments(List, List) + * @see #checkArguments * @param invokedMethod the method type to be invoked * @param tree method or constructor invocation tree */ @@ -1667,22 +1685,31 @@ public Void visitNewClass(NewClassTree node, Void p) { } ParameterizedExecutableType fromUse = atypeFactory.constructorFromUse(node); - AnnotatedExecutableType constructor = fromUse.executableType; + AnnotatedExecutableType constructorType = fromUse.executableType; List typeargs = fromUse.typeArgs; List passedArguments = node.getArguments(); List params = - AnnotatedTypes.expandVarArgs(atypeFactory, constructor, passedArguments); + AnnotatedTypes.expandVarArgs(atypeFactory, constructorType, passedArguments); - checkArguments(params, passedArguments); - checkVarargs(constructor, node); + ExecutableElement constructor = constructorType.getElement(); + Name constructorName = constructor.getSimpleName(); + + checkArguments(params, passedArguments, constructorName, constructor.getParameters()); + checkVarargs(constructorType, node); List paramBounds = new ArrayList<>(); - for (AnnotatedTypeVariable param : constructor.getTypeVariables()) { + for (AnnotatedTypeVariable param : constructorType.getTypeVariables()) { paramBounds.add(param.getBounds()); } - checkTypeArguments(node, paramBounds, typeargs, node.getTypeArguments()); + checkTypeArguments( + node, + paramBounds, + typeargs, + node.getTypeArguments(), + constructorName, + constructor.getTypeParameters()); boolean valid = validateTypeOf(node); @@ -1691,7 +1718,7 @@ public Void visitNewClass(NewClassTree node, Void p) { if (atypeFactory.getDependentTypesHelper() != null) { atypeFactory.getDependentTypesHelper().checkType(dt, node); } - checkConstructorInvocation(dt, constructor, node); + checkConstructorInvocation(dt, constructorType, node); } // Do not call super, as that would observe the arguments without // a set assignment context. @@ -2338,11 +2365,14 @@ protected Set getThrowUpperBoundAnnotations() { * * @param varTree the AST node for the lvalue (usually a variable) * @param valueExp the AST node for the rvalue (the new value) - * @param errorKey the error message to use if the check fails (must be a compiler message key, - * see {@link org.checkerframework.checker.compilermsgs.qual.CompilerMessageKey}) + * @param errorKey the error message key to use if the check fails + * @param extraArgs arguments to the error message key, before "found" and "expected" types */ protected void commonAssignmentCheck( - Tree varTree, ExpressionTree valueExp, @CompilerMessageKey String errorKey) { + Tree varTree, + ExpressionTree valueExp, + @CompilerMessageKey String errorKey, + Object... extraArgs) { AnnotatedTypeMirror var = atypeFactory.getAnnotatedTypeLhs(varTree); assert var != null : "no variable found for tree: " + varTree; @@ -2350,7 +2380,7 @@ protected void commonAssignmentCheck( return; } - commonAssignmentCheck(var, valueExp, errorKey); + commonAssignmentCheck(var, valueExp, errorKey, extraArgs); } /** @@ -2359,13 +2389,14 @@ protected void commonAssignmentCheck( * * @param varType the annotated type of the lvalue (usually a variable) * @param valueExp the AST node for the rvalue (the new value) - * @param errorKey the error message to use if the check fails (must be a compiler message key, - * see {@link org.checkerframework.checker.compilermsgs.qual.CompilerMessageKey}) + * @param errorKey the error message key to use if the check fails + * @param extraArgs arguments to the error message key, before "found" and "expected" types */ protected void commonAssignmentCheck( AnnotatedTypeMirror varType, ExpressionTree valueExp, - @CompilerMessageKey String errorKey) { + @CompilerMessageKey String errorKey, + Object... extraArgs) { if (shouldSkipUses(valueExp)) { return; } @@ -2389,7 +2420,7 @@ protected void commonAssignmentCheck( } AnnotatedTypeMirror valueType = atypeFactory.getAnnotatedType(valueExp); assert valueType != null : "null type for expression: " + valueExp; - commonAssignmentCheck(varType, valueType, valueExp, errorKey); + commonAssignmentCheck(varType, valueType, valueExp, errorKey, extraArgs); } /** @@ -2483,14 +2514,15 @@ protected final void commonAssignmentCheckEndDiagnostic( * @param varType the annotated type of the variable * @param valueType the annotated type of the value * @param valueTree the location to use when reporting the error message - * @param errorKey the error message to use if the check fails (must be a compiler message key, - * see {@link org.checkerframework.checker.compilermsgs.qual.CompilerMessageKey}) + * @param errorKey the error message key to use if the check fails + * @param extraArgs arguments to the error message key, before "found" and "expected" types */ protected void commonAssignmentCheck( AnnotatedTypeMirror varType, AnnotatedTypeMirror valueType, Tree valueTree, - @CompilerMessageKey String errorKey) { + @CompilerMessageKey String errorKey, + Object... extraArgs) { commonAssignmentCheckStartDiagnostic(varType, valueType, valueTree); @@ -2519,7 +2551,10 @@ protected void commonAssignmentCheck( FoundRequired pair = FoundRequired.of(valueType, varType); String valueTypeString = pair.found; String varTypeString = pair.required; - checker.reportError(valueTree, errorKey, valueTypeString, varTypeString); + checker.reportError( + valueTree, + errorKey, + SystemUtil.concatenate(extraArgs, valueTypeString, varTypeString)); } } @@ -2668,7 +2703,9 @@ protected void checkTypeArguments( Tree toptree, List paramBounds, List typeargs, - List typeargTrees) { + List typeargTrees, + Name typeOrMethodName, + List paramNames) { // System.out.printf("BaseTypeVisitor.checkTypeArguments: %s, TVs: %s, TAs: %s, TATs: %s%n", // toptree, paramBounds, typeargs, typeargTrees); @@ -2678,19 +2715,17 @@ protected void checkTypeArguments( return; } - assert paramBounds.size() == typeargs.size() + int size = paramBounds.size(); + assert size == typeargs.size() : "BaseTypeVisitor.checkTypeArguments: mismatch between type arguments: " + typeargs + " and type parameter bounds" + paramBounds; - Iterator boundsIter = paramBounds.iterator(); - Iterator argIter = typeargs.iterator(); + for (int i = 0; i < size; i++) { - while (boundsIter.hasNext()) { - - AnnotatedTypeParameterBounds bounds = boundsIter.next(); - AnnotatedTypeMirror typeArg = argIter.next(); + AnnotatedTypeParameterBounds bounds = paramBounds.get(i); + AnnotatedTypeMirror typeArg = typeargs.get(i); if (isIgnoredUninferredWildcard(bounds.getUpperBound()) || isIgnoredUninferredWildcard(typeArg)) { @@ -2713,18 +2748,25 @@ protected void checkTypeArguments( // The type arguments were inferred, report the error on the method invocation. reportErrorToTree = toptree; } else { - reportErrorToTree = typeargTrees.get(typeargs.indexOf(typeArg)); + reportErrorToTree = typeargTrees.get(i); } checkHasQualifierParameterAsTypeArgument(typeArg, paramUpperBound, toptree); commonAssignmentCheck( - paramUpperBound, typeArg, reportErrorToTree, "type.argument.type.incompatible"); + paramUpperBound, + typeArg, + reportErrorToTree, + "type.argument.type.incompatible", + paramNames.get(i), + typeOrMethodName); if (!atypeFactory.getTypeHierarchy().isSubtype(bounds.getLowerBound(), typeArg)) { FoundRequired fr = FoundRequired.of(typeArg, bounds); checker.reportError( reportErrorToTree, "type.argument.type.incompatible", + paramNames.get(i), + typeOrMethodName, fr.found, fr.required); } @@ -2904,26 +2946,50 @@ protected void checkConstructorInvocation( * like var args. * * @see #checkVarargs(AnnotatedTypeMirror.AnnotatedExecutableType, Tree) - * @param requiredArgs the required types + * @param requiredArgs the required types. This may differ from the formal parameter types, + * because it replaces a varargs parameter by multiple parameters with the vararg's element + * type. * @param passedArgs the expressions passed to the corresponding types + * @param executableName the name of the method or constructor being called + * @param paramNames the names of the callee's formal parameters */ protected void checkArguments( List requiredArgs, - List passedArgs) { - assert requiredArgs.size() == passedArgs.size() + List passedArgs, + Name executableName, + List paramNames) { + int size = requiredArgs.size(); + assert size == passedArgs.size() : "mismatch between required args (" + requiredArgs + ") and passed args (" + passedArgs + ")"; + int maxParamNamesIndex = paramNames.size() - 1; + // Rather weak assertion, due to how varargs parameters are treated. + assert size >= maxParamNamesIndex + : String.format( + "mismatched lengths %d %d %d checkArguments(%s, %s, %s, %s)", + size, + passedArgs.size(), + paramNames.size(), + listToString(requiredArgs), + listToString(passedArgs), + executableName, + listToString(paramNames)); Pair preAssCtxt = visitorState.getAssignmentContext(); try { - for (int i = 0; i < requiredArgs.size(); ++i) { + for (int i = 0; i < size; ++i) { visitorState.setAssignmentContext( Pair.of((Tree) null, (AnnotatedTypeMirror) requiredArgs.get(i))); commonAssignmentCheck( - requiredArgs.get(i), passedArgs.get(i), "argument.type.incompatible"); + requiredArgs.get(i), + passedArgs.get(i), + "argument.type.incompatible", + // TODO: for expanded varargs parameters, maybe adjust the name + paramNames.get(Math.min(i, maxParamNamesIndex)), + executableName); // Also descend into the argument within the correct assignment // context. scan(passedArgs.get(i), null); @@ -2933,6 +2999,22 @@ protected void checkArguments( } } + // com.sun.tools.javac.util.List has a toString that does not include surrounding "[...]", + // making it hard to interpret in messages. + /** + * Produce a printed representation of a list, in the standard format with surrounding "[...]". + * + * @param lst a list to format + * @return the printed representation of the list + */ + private String listToString(List lst) { + StringJoiner result = new StringJoiner(",", "[", "]"); + for (Object elt : lst) { + result.add(elt.toString()); + } + return result.toString(); + } + /** * Returns true if both types are type variables and outer contains inner. Outer contains inner * implies: {@literal inner.upperBound <: outer.upperBound outer.lowerBound <: @@ -3857,7 +3939,13 @@ protected MemberSelectTree enclosingMemberSelect() { } } - protected Tree enclosingStatement(Tree tree) { + /** + * Returns the statement that encloses the given one. + * + * @param tree an AST node that is on the current path + * @return the statement that encloses the given one + */ + protected Tree enclosingStatement(@FindDistinct Tree tree) { TreePath path = this.getCurrentPath(); while (path != null && path.getLeaf() != tree) { path = path.getParentPath(); @@ -3900,8 +3988,16 @@ protected void checkAccess(IdentifierTree node, Void p) { } } + /** + * Returns true if access is allowed, based on an @Unused annotation + * + * @param field the field to be accessed, whose declaration might be annotated by @Unused + * @param receiver the expression whose field is accessed + * @param accessTree the access expression + * @return true if access is allowed + */ protected boolean isAccessAllowed( - Element field, AnnotatedTypeMirror receiver, ExpressionTree accessTree) { + Element field, AnnotatedTypeMirror receiver, @FindDistinct ExpressionTree accessTree) { AnnotationMirror unused = atypeFactory.getDeclAnnotation(field, Unused.class); if (unused == null) { return true; diff --git a/framework/src/main/java/org/checkerframework/common/basetype/messages.properties b/framework/src/main/java/org/checkerframework/common/basetype/messages.properties index b93142fb76e..b73e8c09914 100644 --- a/framework/src/main/java/org/checkerframework/common/basetype/messages.properties +++ b/framework/src/main/java/org/checkerframework/common/basetype/messages.properties @@ -13,8 +13,8 @@ vector.copyinto.type.incompatible=incompatible component type in Vector.copyinto return.type.incompatible=incompatible types in return.%ntype of expression: %s%nmethod return type: %s annotation.type.incompatible=incompatible types in annotation.%nfound : %s%nrequired: %s conditional.type.incompatible=incompatible types in conditional expression.%nfound : %s%nrequired: %s -type.argument.type.incompatible=incompatible types in type argument.%nfound : %s%nrequired: %s -argument.type.incompatible=incompatible types in argument.%nfound : %s%nrequired: %s +type.argument.type.incompatible=incompatible type argument for type parameter %s of %s.%nfound : %s%nrequired: %s +argument.type.incompatible=incompatible argument for parameter %s of %s.%nfound : %s%nrequired: %s varargs.type.incompatible=incompatible types in varargs.%nfound : %s%nrequired: %s type.incompatible=incompatible types.%nfound : %s%nrequired: %s bound.type.incompatible=incompatible bounds in %s%ntype: %s%nupper bound: %s%nlower bound: %s @@ -64,14 +64,14 @@ purity.invalid.methodref=Incompatible purity declaration%nMethod%n %s in %s%n purity.invalid.overriding=Incompatible purity declaration%nMethod%n %s in %s%n cannot override%n %s in %s%nfound : %s%nrequired: %s purity.not.deterministic.assign.array=array assignment not allowed in deterministic method purity.not.deterministic.assign.field=field assignment not allowed in deterministic method -purity.not.deterministic.call.method=call to non-deterministic method not allowed in deterministic method +purity.not.deterministic.call.method=call to non-deterministic method %s not allowed in deterministic method purity.not.deterministic.catch=catch block not allowed in deterministic method purity.not.deterministic.object.creation=object creation not allowed in deterministic method purity.not.deterministic.not.sideeffectfree.assign.array=array assignment not allowed in deterministic side-effect-free method purity.not.deterministic.not.sideeffectfree.assign.field=field assignment not allowed in deterministic side-effect-free method -purity.not.deterministic.not.sideeffectfree.call.method=call to non-deterministic non-side-effect-free method not allowed in deterministic side-effect-free method -purity.not.sideeffectfree.call.constructor=call to non-side-effect-free constructor not allowed in side-effect-free method -purity.not.sideeffectfree.call.method=call to non-side-effect-free method not allowed in side-effect-free method +purity.not.deterministic.not.sideeffectfree.call.method=call to non-deterministic side-effecting method %s not allowed in deterministic side-effect-free method +purity.not.sideeffectfree.call.constructor=call to side-effecting constructor not allowed in side-effect-free method +purity.not.sideeffectfree.call.method=call to side-effecting method %s not allowed in side-effect-free method purity.more.deterministic=the method %s could be declared as @Deterministic purity.more.pure=the method %s could be declared as @Pure purity.more.sideeffectfree=the method %s could be declared as @SideEffectFree diff --git a/framework/src/main/java/org/checkerframework/common/reflection/ClassValVisitor.java b/framework/src/main/java/org/checkerframework/common/reflection/ClassValVisitor.java index 378aea8f159..edf142ec25f 100644 --- a/framework/src/main/java/org/checkerframework/common/reflection/ClassValVisitor.java +++ b/framework/src/main/java/org/checkerframework/common/reflection/ClassValVisitor.java @@ -58,8 +58,9 @@ public boolean isValid(AnnotatedTypeMirror type, Tree tree) { } /** - * A string is a legal binary name if it has the following form: ((Java identifier)\.)*(Java - * identifier)([])* https://docs.oracle.com/javase/specs/jls/se11/html/jls-13.html#jls-13.1 + * A string is a binary + * name if it has the following form: ((Java identifier)\.)*(Java identifier)([])* * * @param className string to check * @return true if className is a legal class name diff --git a/framework/src/main/java/org/checkerframework/common/reflection/DefaultReflectionResolver.java b/framework/src/main/java/org/checkerframework/common/reflection/DefaultReflectionResolver.java index d970370fec1..662d12862b6 100644 --- a/framework/src/main/java/org/checkerframework/common/reflection/DefaultReflectionResolver.java +++ b/framework/src/main/java/org/checkerframework/common/reflection/DefaultReflectionResolver.java @@ -132,7 +132,7 @@ private ParameterizedExecutableType resolveMethodCall( // and parameter types for (MethodInvocationTree resolvedTree : possibleMethods) { debugReflection("Resolved method invocation: " + resolvedTree); - if (!checkMethodAgruments(resolvedTree)) { + if (!checkMethodArguments(resolvedTree)) { debugReflection( "Spoofed tree's arguments did not match declaration" + resolvedTree); // Calling methodFromUse on these sorts of trees will cause an assertion to fail in @@ -206,18 +206,40 @@ private ParameterizedExecutableType resolveMethodCall( return origResult; } - private boolean checkMethodAgruments(MethodInvocationTree resolvedTree) { + /** + * Checks that arguments of a method invocation are consistent with their corresponding + * parameters. + * + * @param resolvedTree a method invocation + * @return true if arguments are consistent with parameters + */ + private boolean checkMethodArguments(MethodInvocationTree resolvedTree) { // type.getKind() == actualType.getKind() ExecutableElement methodDecl = TreeUtils.elementFromUse(resolvedTree); - return checkAgruments(methodDecl.getParameters(), resolvedTree.getArguments()); + return checkArguments(methodDecl.getParameters(), resolvedTree.getArguments()); } + /** + * Checks that arguments of a constructor invocation are consistent with their corresponding + * parameters. + * + * @param resolvedTree a constructor invocation + * @return true if arguments are consistent with parameters + */ private boolean checkNewClassArguments(NewClassTree resolvedTree) { ExecutableElement methodDecl = TreeUtils.elementFromUse(resolvedTree); - return checkAgruments(methodDecl.getParameters(), resolvedTree.getArguments()); + return checkArguments(methodDecl.getParameters(), resolvedTree.getArguments()); } - private boolean checkAgruments( + /** + * Checks that argument are consistent with their corresponding parameter types. Common code + * used by {@link #checkMethodArguments} and {@link #checkNewClassArguments}. + * + * @param parameters formal parameters + * @param arguments actual arguments + * @return true if argument are consistent with their corresponding parameter types + */ + private boolean checkArguments( List parameters, List arguments) { if (parameters.size() != arguments.size()) { return false; @@ -478,8 +500,13 @@ private boolean isUnknownMethod(MethodInvocationTree tree) { /** * Get set of MethodSymbols based on class name, method name, and parameter length. * + * @param className the class that contains the method + * @param methodName the method's name + * @param paramLength the number of parameters + * @param env the environment * @return the (potentially empty) set of corresponding method Symbol(s) */ + @SuppressWarnings("interning:not.interned") // bug? private List getMethodSymbolsfor( String className, String methodName, int paramLength, Env env) { Context context = ((JavacProcessingEnvironment) processingEnv).getContext(); diff --git a/framework/src/main/java/org/checkerframework/common/subtyping/SubtypingChecker.java b/framework/src/main/java/org/checkerframework/common/subtyping/SubtypingChecker.java index 1ce0e373aac..160007f196d 100644 --- a/framework/src/main/java/org/checkerframework/common/subtyping/SubtypingChecker.java +++ b/framework/src/main/java/org/checkerframework/common/subtyping/SubtypingChecker.java @@ -16,7 +16,7 @@ * *

          *
        • {@code -Aquals}: specifies the annotations in the qualifier hierarchy (as a comma-separated - * list of fully-qualified annotation names with no spaces in between). Only the annotation + * list of fully-qualified annotation names with no spaces in between). Only the annotations * for one qualified subtype hierarchy can be passed. *
        * diff --git a/framework/src/main/java/org/checkerframework/common/util/TypeVisualizer.java b/framework/src/main/java/org/checkerframework/common/util/TypeVisualizer.java index c4910d0556f..a1fff2c58cc 100644 --- a/framework/src/main/java/org/checkerframework/common/util/TypeVisualizer.java +++ b/framework/src/main/java/org/checkerframework/common/util/TypeVisualizer.java @@ -12,6 +12,8 @@ import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.TypeParameterElement; import javax.lang.model.element.VariableElement; +import org.checkerframework.checker.interning.qual.FindDistinct; +import org.checkerframework.checker.interning.qual.InternedDistinct; import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.framework.type.AnnotatedTypeMirror; import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedArrayType; @@ -174,9 +176,15 @@ private static boolean printTypevarIfMatches( * create a wrapper that performed referential equality on types and use a LinkedHashMap. */ private static class Node { - private final AnnotatedTypeMirror type; + /** The delegate; that is, the wrapped value. */ + private final @InternedDistinct AnnotatedTypeMirror type; - private Node(final AnnotatedTypeMirror type) { + /** + * Create a new Node that wraps the given type. + * + * @param type the type that the newly-constructed Node represents + */ + private Node(final @FindDistinct AnnotatedTypeMirror type) { this.type = type; } diff --git a/framework/src/main/java/org/checkerframework/common/util/debug/TypeOutputtingChecker.java b/framework/src/main/java/org/checkerframework/common/util/debug/TypeOutputtingChecker.java index 389081fdc24..71d7de16d6b 100644 --- a/framework/src/main/java/org/checkerframework/common/util/debug/TypeOutputtingChecker.java +++ b/framework/src/main/java/org/checkerframework/common/util/debug/TypeOutputtingChecker.java @@ -234,14 +234,6 @@ public boolean isSubtype(AnnotationMirror subAnno, AnnotationMirror superAnno) { throw new BugInCF("GeneralQualifierHierarchy.isSubtype() shouldn't be called."); } - // Not needed - raises error. - @Override - public boolean isSubtypeTypeVariable( - AnnotationMirror subAnno, AnnotationMirror superAnno) { - throw new BugInCF( - "GeneralQualifierHierarchy.isSubtypeTypeVariable() shouldn't be called."); - } - // Not needed - raises error. @Override public boolean isSubtype( @@ -250,15 +242,6 @@ public boolean isSubtype( throw new BugInCF("GeneralQualifierHierarchy.isSubtype() shouldn't be called."); } - // Not needed - raises error. - @Override - public boolean isSubtypeTypeVariable( - Collection subAnnos, - Collection superAnnos) { - throw new BugInCF( - "GeneralQualifierHierarchy.isSubtypeTypeVariable() shouldn't be called."); - } - // Not needed - raises error. @Override public AnnotationMirror leastUpperBound(AnnotationMirror a1, AnnotationMirror a2) { @@ -266,14 +249,6 @@ public AnnotationMirror leastUpperBound(AnnotationMirror a1, AnnotationMirror a2 "GeneralQualifierHierarchy.leastUpperBound() shouldn't be called."); } - // Not needed - raises error. - @Override - public AnnotationMirror leastUpperBoundTypeVariable( - AnnotationMirror a1, AnnotationMirror a2) { - throw new BugInCF( - "GeneralQualifierHierarchy.leastUpperBoundTypeVariable() shouldn't be called."); - } - // Not needed - raises error. @Override public AnnotationMirror greatestLowerBound(AnnotationMirror a1, AnnotationMirror a2) { @@ -281,14 +256,6 @@ public AnnotationMirror greatestLowerBound(AnnotationMirror a1, AnnotationMirror "GeneralQualifierHierarchy.greatestLowerBound() shouldn't be called."); } - // Not needed - raises error. - @Override - public AnnotationMirror greatestLowerBoundTypeVariable( - AnnotationMirror a1, AnnotationMirror a2) { - throw new BugInCF( - "GeneralQualifierHierarchy.greatestLowerBoundTypeVariable() shouldn't be called."); - } - @Override public AnnotationMirror getPolymorphicAnnotation(AnnotationMirror start) { throw new BugInCF( diff --git a/framework/src/main/java/org/checkerframework/common/value/ValueAnnotatedTypeFactory.java b/framework/src/main/java/org/checkerframework/common/value/ValueAnnotatedTypeFactory.java index cb98ed06642..3f42d820324 100644 --- a/framework/src/main/java/org/checkerframework/common/value/ValueAnnotatedTypeFactory.java +++ b/framework/src/main/java/org/checkerframework/common/value/ValueAnnotatedTypeFactory.java @@ -1229,6 +1229,18 @@ public int getMinLenFromString(String sequenceExpression, Tree tree, TreePath cu (FlowExpressions.ArrayCreation) expressionObj; // This is only expected to support array creations in varargs methods return arrayCreation.getInitializers().size(); + } else if (expressionObj instanceof FlowExpressions.ArrayAccess) { + List annoList = + expressionObj.getType().getAnnotationMirrors(); + for (AnnotationMirror anno : annoList) { + String ANNO_NAME = anno.getAnnotationType().toString(); + if (ANNO_NAME.equals(MINLEN_NAME)) { + return getMinLenValue(canonicalAnnotation(anno)); + } else if (ANNO_NAME.equals(ARRAYLEN_NAME) + || ANNO_NAME.equals(ARRAYLENRANGE_NAME)) { + return getMinLenValue(anno); + } + } } lengthAnno = getAnnotationFromReceiver(expressionObj, tree, ArrayLenRange.class); diff --git a/framework/src/main/java/org/checkerframework/common/value/ValueMethodIdentifier.java b/framework/src/main/java/org/checkerframework/common/value/ValueMethodIdentifier.java index d8f5bf7b606..e410c1c7a07 100644 --- a/framework/src/main/java/org/checkerframework/common/value/ValueMethodIdentifier.java +++ b/framework/src/main/java/org/checkerframework/common/value/ValueMethodIdentifier.java @@ -19,13 +19,21 @@ class ValueMethodIdentifier { private final List mathMinMethod; /** The {@code java.lang.Math#max()} methods. */ private final List mathMaxMethod; + /** Arrays.copyOf() methods. */ + private final List copyOfMethod; + /** + * Initialize elements with methods that have special handling in the value checker + * + * @param processingEnv the processing environment + */ public ValueMethodIdentifier(ProcessingEnvironment processingEnv) { lengthMethod = TreeUtils.getMethod("java.lang.String", "length", 0, processingEnv); startsWithMethod = TreeUtils.getMethod("java.lang.String", "startsWith", 1, processingEnv); endsWithMethod = TreeUtils.getMethod("java.lang.String", "endsWith", 1, processingEnv); mathMinMethod = TreeUtils.getMethods("java.lang.Math", "min", 2, processingEnv); mathMaxMethod = TreeUtils.getMethods("java.lang.Math", "max", 2, processingEnv); + copyOfMethod = TreeUtils.getMethods("java.util.Arrays", "copyOf", 2, processingEnv); } /** Returns true iff the argument is an invocation of Math.min. */ @@ -59,4 +67,15 @@ public boolean isEndsWithMethod(ExecutableElement method) { // equals (rather than ElementUtils.ismethod) because String.length cannot be overridden return method.equals(endsWithMethod); } + + /** + * Determines whether a tree is an invocation of the {@code Arrays.copyOf()} method. + * + * @param tree tree to check + * @param processingEnv the processing environment + * @return true iff the argument is an invocation of {@code Arrays.copyOf()} method. + */ + public boolean isArraysCopyOfInvocation(Tree tree, ProcessingEnvironment processingEnv) { + return TreeUtils.isMethodInvocation(tree, copyOfMethod, processingEnv); + } } diff --git a/framework/src/main/java/org/checkerframework/common/value/ValueTreeAnnotator.java b/framework/src/main/java/org/checkerframework/common/value/ValueTreeAnnotator.java index fce32844d7f..f51f84b2f2c 100644 --- a/framework/src/main/java/org/checkerframework/common/value/ValueTreeAnnotator.java +++ b/framework/src/main/java/org/checkerframework/common/value/ValueTreeAnnotator.java @@ -17,6 +17,7 @@ import org.checkerframework.framework.type.AnnotatedTypeMirror; import org.checkerframework.framework.type.treeannotator.TreeAnnotator; 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; @@ -398,6 +399,22 @@ public Void visitMethodInvocation(MethodInvocationTree tree, AnnotatedTypeMirror } } + if (atypeFactory + .getMethodIdentifier() + .isArraysCopyOfInvocation(tree, atypeFactory.getProcessingEnv())) { + List args = tree.getArguments(); + if (args.size() != 2) { + throw new BugInCF( + "Arrays.copyOf() should have 2 arguments. This point should not have reached"); + } + Range range = + ValueCheckerUtils.getPossibleValues( + atypeFactory.getAnnotatedType(args.get(1)), atypeFactory); + if (range != null) { + type.replaceAnnotation(atypeFactory.createArrayLenRangeAnnotation(range)); + } + } + if (!methodIsStaticallyExecutable(TreeUtils.elementFromUse(tree)) || !handledByValueChecker(type)) { return null; diff --git a/framework/src/main/java/org/checkerframework/common/value/ValueVisitor.java b/framework/src/main/java/org/checkerframework/common/value/ValueVisitor.java index 5e563f6b129..6aee2b3424d 100644 --- a/framework/src/main/java/org/checkerframework/common/value/ValueVisitor.java +++ b/framework/src/main/java/org/checkerframework/common/value/ValueVisitor.java @@ -46,16 +46,18 @@ public ValueVisitor(BaseTypeChecker checker) { * * @param varType the annotated type of the lvalue (usually a variable) * @param valueExp the AST node for the rvalue (the new value) - * @param errorKey the error message to use if the check fails (must be a compiler message key, + * @param errorKey the error message key to use if the check fails + * @param extraArgs arguments to the error message key, before "found" and "expected" types */ @Override protected void commonAssignmentCheck( AnnotatedTypeMirror varType, ExpressionTree valueExp, - @CompilerMessageKey String errorKey) { + @CompilerMessageKey String errorKey, + Object... extraArgs) { replaceSpecialIntRangeAnnotations(varType); - super.commonAssignmentCheck(varType, valueExp, errorKey); + super.commonAssignmentCheck(varType, valueExp, errorKey, extraArgs); } @Override @@ -63,7 +65,8 @@ protected void commonAssignmentCheck( AnnotatedTypeMirror varType, AnnotatedTypeMirror valueType, Tree valueTree, - @CompilerMessageKey String errorKey) { + @CompilerMessageKey String errorKey, + Object... extraArgs) { replaceSpecialIntRangeAnnotations(varType); @@ -73,13 +76,14 @@ protected void commonAssignmentCheck( getTypeFactory().createIntRangeAnnotation(Range.CHAR_EVERYTHING)); } - super.commonAssignmentCheck(varType, valueType, valueTree, errorKey); + super.commonAssignmentCheck(varType, valueType, valueTree, errorKey, extraArgs); } /** * Return types for methods that are annotated with {@code @IntRangeFromX} annotations need to * be replaced with {@code @UnknownVal}. See the documentation on {@link - * #commonAssignmentCheck(AnnotatedTypeMirror, ExpressionTree, String) commonAssignmentCheck}. + * #commonAssignmentCheck(AnnotatedTypeMirror, ExpressionTree, String, Object[]) + * commonAssignmentCheck}. * *

        A separate override is necessary because checkOverride doesn't actually use the * commonAssignmentCheck. diff --git a/framework/src/main/java/org/checkerframework/common/value/util/Range.java b/framework/src/main/java/org/checkerframework/common/value/util/Range.java index b373a081f78..c242ff9c5f7 100644 --- a/framework/src/main/java/org/checkerframework/common/value/util/Range.java +++ b/framework/src/main/java/org/checkerframework/common/value/util/Range.java @@ -7,6 +7,7 @@ import java.util.List; import java.util.Objects; import javax.lang.model.type.TypeKind; +import org.checkerframework.checker.interning.qual.InternedDistinct; import org.checkerframework.checker.nullness.qual.Nullable; /** @@ -58,8 +59,10 @@ public class Range { /** A range containing all possible 8-bit values. */ public static final Range BYTE_EVERYTHING = create(Byte.MIN_VALUE, Byte.MAX_VALUE); - /** The empty range singleton. */ - public static final Range NOTHING = new Range(Long.MAX_VALUE, Long.MIN_VALUE); + /** The empty range. This is the only Range object that contains nothing */ + @SuppressWarnings( + "interning:assignment.type.incompatible") // no other constructor call makes this + public static final @InternedDistinct Range NOTHING = new Range(Long.MAX_VALUE, Long.MIN_VALUE); /** An alias to the range containing all possible 64-bit values. */ public static final Range EVERYTHING = LONG_EVERYTHING; diff --git a/framework/src/main/java/org/checkerframework/common/wholeprograminference/SceneToStubWriter.java b/framework/src/main/java/org/checkerframework/common/wholeprograminference/SceneToStubWriter.java index 4ce80c0148f..32bf572bb8c 100644 --- a/framework/src/main/java/org/checkerframework/common/wholeprograminference/SceneToStubWriter.java +++ b/framework/src/main/java/org/checkerframework/common/wholeprograminference/SceneToStubWriter.java @@ -14,15 +14,18 @@ import java.util.StringJoiner; import java.util.regex.Pattern; import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.Element; import javax.lang.model.element.TypeElement; import javax.lang.model.element.TypeParameterElement; import javax.lang.model.element.VariableElement; import javax.lang.model.type.ArrayType; import javax.lang.model.type.TypeKind; import javax.lang.model.type.TypeMirror; +import org.checkerframework.checker.index.qual.SameLen; import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.checker.signature.qual.BinaryName; import org.checkerframework.checker.signature.qual.DotSeparatedIdentifiers; +import org.checkerframework.common.value.qual.MinLen; import org.checkerframework.common.wholeprograminference.scenelib.ASceneWrapper; import org.checkerframework.javacutil.AnnotationUtils; import org.checkerframework.javacutil.BugInCF; @@ -71,10 +74,14 @@ public final class SceneToStubWriter { private SceneToStubWriter() {} /** - * A pattern matching the name of an anonymous inner class, or a class nested within one. An - * anonymous inner class has a basename like Outer$1. + * A pattern matching the name of an anonymous inner class, a local class, or a class nested + * within one of these types of classes. An anonymous inner class has a basename like Outer$1 + * and a local class has a basename like Outer$1Inner. See Java Language + * Specification, section 13.1. */ - private static final Pattern anonymousInnerClassPattern = Pattern.compile("\\$\\d+(\\$|$)"); + private static final Pattern anonymousInnerClassOrLocalClassPattern = + Pattern.compile("\\$\\d+"); /** How far to indent when writing members of a stub file. */ private static final String INDENT = " "; @@ -200,9 +207,13 @@ private static String formatArrayType(ATypeElement scenelibRep, ArrayType javacR * @param levels the number of component types the type should have, derived from the javac * representation * @return a list of the array levels in scenelib's representation, but in the order used by - * javac. Guaranteed to have exactly {@code levels} entries. + * javac. Guaranteed to have exactly {@code levels} entries. Entries may be null, if the + * corresponding parts of {@code scenelibRep} are null. See issue 3422 for an + * example of code that causes a null ATypeElement, because the component type is unknown, + * but the primary type of the array is known. */ - private static List getSceneLibRepInJavacOrder( + private static List<@Nullable ATypeElement> getSceneLibRepInJavacOrder( ATypeElement scenelibRep, int levels) { List result = new ArrayList<>(); ATypeElement array = scenelibRep; @@ -225,12 +236,13 @@ private static List getSceneLibRepInJavacOrder( * using {@link #formatType(ATypeElement, TypeMirror)}. * * @param scenelibRepInJavacOrder the scenelib representation, reordered to match javac's order. - * See {@link #getSceneLibRepInJavacOrder} for an explanation of why this is necessary. + * See {@link #getSceneLibRepInJavacOrder} for an explanation of why this is necessary and + * why the elements may be null. * @param javacRep the javac representation of the array type * @return the type formatted to be written to Java source code, followed by a space character */ private static String formatArrayTypeImpl( - List scenelibRepInJavacOrder, ArrayType javacRep) { + List<@Nullable ATypeElement> scenelibRepInJavacOrder, ArrayType javacRep) { TypeMirror javacComponent = javacRep.getComponentType(); ATypeElement scenelibRep = scenelibRepInJavacOrder.get(0); ATypeElement scenelibComponent = scenelibRepInJavacOrder.get(1); @@ -240,7 +252,7 @@ private static String formatArrayTypeImpl( result += explicitAnno.toString(); result += " "; } - if ("".equals(result)) { + if (result.isEmpty() && scenelibRep != null) { result += formatAnnotations(scenelibRep.tlAnnotationsHere); } result += "[] "; @@ -450,6 +462,11 @@ private static boolean isInternalJDKAnnotation(String annotationName) { private static int printClassDefinitions( String basename, AClass aClass, PrintWriter printWriter) { String[] classNames = basename.split("\\$"); + TypeElement innermostTypeElt = aClass.getTypeElement(); + if (innermostTypeElt == null) { + throw new BugInCF("typeElement was unexpectedly null in this aClass: " + aClass); + } + TypeElement[] typeElements = getTypeElementsForClasses(innermostTypeElt, classNames); for (int i = 0; i < classNames.length; i++) { String nameToPrint = classNames[i]; @@ -459,9 +476,14 @@ private static int printClassDefinitions( } else { printWriter.print("class "); } - printWriter.print(formatAnnotations(aClass.getAnnotations())); + if (i == classNames.length - 1) { + // Only print class annotations on the innermost class, which corresponds to aClass. + // If there should be class annotations on another class, it will have its own stub + // file, which will eventually be merged with this one. + printWriter.print(formatAnnotations(aClass.getAnnotations())); + } printWriter.print(nameToPrint); - printTypeParameters(aClass, printWriter); + printTypeParameters(typeElements[i], printWriter); printWriter.println(" {"); if (aClass.isEnum(nameToPrint) && i != classNames.length - 1) { // Print a blank set of enum constants if this is an outer enum. @@ -472,6 +494,27 @@ private static int printClassDefinitions( return classNames.length; } + /** + * Constructs an array of TypeElements corresponding to the list of classes. + * + * @param innermostTypeElt the innermost type element: either an inner class or an outer class + * without any inner classes that should be printed + * @param classNames the names of the containing classes, from outer to inner + * @return an array of TypeElements whose entry at a given index represents the type named at + * that index in {@code classNames} + */ + private static TypeElement @SameLen("#2") [] getTypeElementsForClasses( + TypeElement innermostTypeElt, String @MinLen(1) [] classNames) { + TypeElement[] result = new TypeElement[classNames.length]; + result[classNames.length - 1] = innermostTypeElt; + Element elt = innermostTypeElt; + for (int i = classNames.length - 2; i >= 0; i--) { + elt = elt.getEnclosingElement(); + result[i] = (TypeElement) elt; + } + return result; + } + /** * Prints all the fields of a given class. * @@ -636,9 +679,9 @@ private static boolean isPrintable(@BinaryName String classname, AClass aClass) return false; } - // Do not attempt to print stubs for anonymous inner classes or their inner classes, because - // the stub parser cannot read them. - if (anonymousInnerClassPattern.matcher(basename).find()) { + // Do not attempt to print stubs for anonymous inner classes, local classes, or their inner + // classes, because the stub parser cannot read them. + if (anonymousInnerClassOrLocalClassPattern.matcher(basename).find()) { return false; } @@ -723,14 +766,10 @@ private static String indents(int n) { /** * Prints the type parameters of the given class, enclosed in {@code <...>}. * - * @param aClass the class whose type parameters should be printed + * @param type the TypeElement representing the class whose type parameters should be printed * @param printWriter where to print the type parameters */ - private static void printTypeParameters(AClass aClass, PrintWriter printWriter) { - TypeElement type = aClass.getTypeElement(); - if (type == null) { - return; - } + private static void printTypeParameters(TypeElement type, PrintWriter printWriter) { List typeParameters = type.getTypeParameters(); printTypeParameters(typeParameters, printWriter); } diff --git a/framework/src/main/java/org/checkerframework/common/wholeprograminference/WholeProgramInferenceScenes.java b/framework/src/main/java/org/checkerframework/common/wholeprograminference/WholeProgramInferenceScenes.java index c1644d5bbf5..9cd29c88fa9 100644 --- a/framework/src/main/java/org/checkerframework/common/wholeprograminference/WholeProgramInferenceScenes.java +++ b/framework/src/main/java/org/checkerframework/common/wholeprograminference/WholeProgramInferenceScenes.java @@ -9,6 +9,7 @@ import com.sun.tools.javac.code.Symbol.VarSymbol; import java.util.List; import java.util.Map; +import javax.lang.model.SourceVersion; import javax.lang.model.element.Element; import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.VariableElement; @@ -229,7 +230,11 @@ public void updateFromLocalAssignment( String className = getEnclosingClassName(lhs); String jaifPath = storage.getJaifPath(className); - AClass clazz = storage.getAClass(className, jaifPath); + AClass clazz = + storage.getAClass( + className, + jaifPath, + (ClassSymbol) TreeUtils.elementFromDeclaration(classTree)); ExecutableElement methodElt = TreeUtils.elementFromDeclaration(methodTree); AMethod method = clazz.methods.getVivify(JVMNames.getJVMMethodSignature(methodElt)); method.setFieldsFromMethodElement(methodElt); @@ -279,6 +284,16 @@ public void updateFromFieldAssignment( + lhs.getClass()); } + // Do not attempt to infer types for fields that do not have valid + // names. For example, compiler-generated temporary variables will + // have invalid names. Recording facts about fields with + // invalid names causes jaif-based WPI to crash when reading the .jaif + // file, and stub-based WPI to generate unparseable stub files. + // See https://github.com/typetools/checker-framework/issues/3442 + if (!SourceVersion.isIdentifier(fieldName)) { + return; + } + // If the inferred field has a declaration annotation with the // @IgnoreInWholeProgramInference meta-annotation, exit this routine. if (atf.getDeclAnnotation(element, IgnoreInWholeProgramInference.class) != null diff --git a/framework/src/main/java/org/checkerframework/common/wholeprograminference/scenelib/ASceneWrapper.java b/framework/src/main/java/org/checkerframework/common/wholeprograminference/scenelib/ASceneWrapper.java index fd07eb3b7db..d292eab76b8 100644 --- a/framework/src/main/java/org/checkerframework/common/wholeprograminference/scenelib/ASceneWrapper.java +++ b/framework/src/main/java/org/checkerframework/common/wholeprograminference/scenelib/ASceneWrapper.java @@ -163,7 +163,7 @@ public void writeToFile( /** * Updates the symbol information stored in AClass for the given class. May be called multiple * times (and needs to be if the second parameter was null the first time it was called; only - * some calls provide the symbol inforamtion). + * some calls provide the symbol information). * * @param aClass the class representation in which the symbol information is to be updated * @param classSymbol the source of the symbol information; may be null, in which case this diff --git a/framework/src/main/java/org/checkerframework/framework/flow/CFAbstractStore.java b/framework/src/main/java/org/checkerframework/framework/flow/CFAbstractStore.java index b2c185de657..2be57ad76ca 100644 --- a/framework/src/main/java/org/checkerframework/framework/flow/CFAbstractStore.java +++ b/framework/src/main/java/org/checkerframework/framework/flow/CFAbstractStore.java @@ -1025,7 +1025,7 @@ public String visualize(CFGVisualizer viz) { @SuppressWarnings("unchecked") CFGVisualizer castedViz = (CFGVisualizer) viz; StringBuilder sbVisualize = new StringBuilder(); - sbVisualize.append(castedViz.visualizeStoreHeader(this.getClass().getCanonicalName())); + sbVisualize.append(castedViz.visualizeStoreHeader(this.getClass().getSimpleName())); sbVisualize.append(internalVisualize(castedViz)); sbVisualize.append(castedViz.visualizeStoreFooter()); return sbVisualize.toString(); diff --git a/framework/src/main/java/org/checkerframework/framework/flow/CFAbstractTransfer.java b/framework/src/main/java/org/checkerframework/framework/flow/CFAbstractTransfer.java index 5f68c62d5de..fd73f878289 100644 --- a/framework/src/main/java/org/checkerframework/framework/flow/CFAbstractTransfer.java +++ b/framework/src/main/java/org/checkerframework/framework/flow/CFAbstractTransfer.java @@ -20,6 +20,7 @@ import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.VariableElement; import javax.lang.model.type.TypeMirror; +import org.checkerframework.checker.interning.qual.InternedDistinct; import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.dataflow.analysis.ConditionalTransferResult; import org.checkerframework.dataflow.analysis.FlowExpressions; @@ -156,6 +157,11 @@ protected V finishValue(V value, S store) { * transfer function. By default, the value is not changed but subclasses might decide to * implement some functionality. The store at this position is also passed (two stores, as the * result is a {@link ConditionalTransferResult}. + * + * @param value the value to finish + * @param thenStore the "then" store + * @param elseStore the "else" store + * @return the finished value */ protected V finishValue(V value, S thenStore, S elseStore) { return value; @@ -314,7 +320,8 @@ public S initialStore( } CFGLambda lambda = (CFGLambda) underlyingAST; - Tree enclosingTree = + @SuppressWarnings("interning:assignment.type.incompatible") // used in == tests + @InternedDistinct Tree enclosingTree = TreeUtils.enclosingOfKind( factory.getPath(lambda.getLambdaTree()), new HashSet<>( diff --git a/framework/src/main/java/org/checkerframework/framework/flow/CFAbstractValue.java b/framework/src/main/java/org/checkerframework/framework/flow/CFAbstractValue.java index ca8fb7eaed9..88c1420f743 100644 --- a/framework/src/main/java/org/checkerframework/framework/flow/CFAbstractValue.java +++ b/framework/src/main/java/org/checkerframework/framework/flow/CFAbstractValue.java @@ -2,6 +2,7 @@ import java.util.Objects; import java.util.Set; +import java.util.StringJoiner; import javax.annotation.processing.ProcessingEnvironment; import javax.lang.model.element.AnnotationMirror; import javax.lang.model.type.TypeKind; @@ -18,6 +19,7 @@ import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedWildcardType; import org.checkerframework.framework.type.QualifierHierarchy; import org.checkerframework.framework.util.AnnotatedTypes; +import org.checkerframework.framework.util.DefaultAnnotationFormatter; import org.checkerframework.javacutil.AnnotationUtils; import org.checkerframework.javacutil.SystemUtil; import org.checkerframework.javacutil.TypesUtils; @@ -51,9 +53,18 @@ public abstract class CFAbstractValue> implements A /** The analysis class this value belongs to. */ protected final CFAbstractAnalysis analysis; + /** The underlying (Java) type in this abstract value. */ protected final TypeMirror underlyingType; + /** The annotations in this abstract value. */ protected final Set annotations; + /** + * Creates a new CFAbstractValue. + * + * @param analysis the analysis class this value belongs to + * @param annotations the annotations in this abstract value + * @param underlyingType the underlying (Java) type in this abstract value + */ protected CFAbstractValue( CFAbstractAnalysis analysis, Set annotations, @@ -136,6 +147,7 @@ public TypeMirror getUnderlyingType() { return underlyingType; } + @SuppressWarnings("interning:not.interned") // efficiency pre-test @Override public boolean equals(@Nullable Object obj) { if (!(obj instanceof CFAbstractValue)) { @@ -158,19 +170,41 @@ public int hashCode() { } /** - * Returns the string representation as a comma-separated list. + * Returns the string representation, using fully-qualified names. + * + * @return the string representation, using fully-qualified names + */ + @SideEffectFree + public String toStringFullyQualified() { + return "CFAV{" + annotations + ", " + underlyingType + '}'; + } + + /** + * Returns the string representation, using simple (not fully-qualified) names. + * + * @return the string representation, using simple (not fully-qualified) names + */ + @SideEffectFree + public String toStringSimple() { + + DefaultAnnotationFormatter defaultAnnotationFormatter = new DefaultAnnotationFormatter(); + StringJoiner annotationsString = new StringJoiner(", "); + for (AnnotationMirror am : annotations) { + annotationsString.add(defaultAnnotationFormatter.formatAnnotationMirror(am)); + } + + return "CFAV{" + annotationsString + ", " + TypesUtils.simpleTypeName(underlyingType) + '}'; + } + + /** + * Returns the string representation. * - * @return the string representation as a comma-separated list + * @return the string representation */ @SideEffectFree @Override public String toString() { - return "CFAbstractValue{" - + "annotations=" - + annotations - + ", underlyingType=" - + underlyingType - + '}'; + return toStringSimple(); } /** diff --git a/framework/src/main/java/org/checkerframework/framework/qual/Covariant.java b/framework/src/main/java/org/checkerframework/framework/qual/Covariant.java index 58b91c20988..ac79c2e99db 100644 --- a/framework/src/main/java/org/checkerframework/framework/qual/Covariant.java +++ b/framework/src/main/java/org/checkerframework/framework/qual/Covariant.java @@ -15,14 +15,15 @@ *

        Ordinarily, Java treats type parameters invariantly: {@code SomeClass} is unrelated to * (neither a subtype nor a supertype of) {@code SomeClass}. * - *

        It is only safe to mark a type parameter as covariant if the type parameter is used in a - * read-only way: values of that type are read from but never modified. This property is not - * checked; the {@code @Covariant} is simply trusted. + *

        It is only safe to mark a type parameter as covariant if clients use the type parameter in a + * read-only way: clients read values of that type but never modify them. + * + *

        This property is not checked; the {@code @Covariant} is simply trusted. * *

        Here is an example use: * *

        {@code @Covariant(0)
        - *  public interface Iterator { ... }
        + * public interface Iterator { ... }
          * }
        * * @checker_framework.manual #covariant-type-parameters Covariant type parameters diff --git a/framework/src/main/java/org/checkerframework/framework/qual/DefaultFor.java b/framework/src/main/java/org/checkerframework/framework/qual/DefaultFor.java index 7aad2178ba0..6d695562873 100644 --- a/framework/src/main/java/org/checkerframework/framework/qual/DefaultFor.java +++ b/framework/src/main/java/org/checkerframework/framework/qual/DefaultFor.java @@ -7,15 +7,17 @@ import java.lang.annotation.Target; /** - * A meta-annotation applied to the declaration of a type qualifier specifies that the given - * annotation should be the default for. + * A meta-annotation applied to the declaration of a type qualifier. It specifies that the given + * annotation should be the default for: * *
          - *
        • a particular location. - *
        • a use of a particular type. - *
        • a use of a particular kind of type. + *
        • all uses at a particular location, + *
        • all uses of a particular type, and + *
        • all uses of a particular kind of type. *
        * + *

        The default applies to every match for any of this annotation's conditions. + * * @see TypeUseLocation * @see DefaultQualifier * @see DefaultQualifierInHierarchy diff --git a/framework/src/main/java/org/checkerframework/framework/qual/PolymorphicQualifier.java b/framework/src/main/java/org/checkerframework/framework/qual/PolymorphicQualifier.java index efb664a822c..e3c2573dc20 100644 --- a/framework/src/main/java/org/checkerframework/framework/qual/PolymorphicQualifier.java +++ b/framework/src/main/java/org/checkerframework/framework/qual/PolymorphicQualifier.java @@ -23,10 +23,12 @@ /** * Indicates which type system this annotation refers to (optional, and usually unnecessary). * When multiple type hierarchies are supported by a single type system, then each polymorphic - * qualifier needs to indicate which sub-hierarchy it belongs to. Do so by passing a qualifier - * from the given hierarchy, by convention the top qualifier. + * qualifier needs to indicate which sub-hierarchy it belongs to. Do so by passing the top + * qualifier from the given hierarchy. + * + * @return the top qualifier in the hierarchy of this qualifier */ - // We use the meaningless PolymorphicQualifier.class as default value and + // We use the meaningless Annotation.class as default value and // then ensure there is a single top qualifier to use. - Class value() default PolymorphicQualifier.class; + Class value() default Annotation.class; } diff --git a/framework/src/main/java/org/checkerframework/framework/qual/SubtypeOf.java b/framework/src/main/java/org/checkerframework/framework/qual/SubtypeOf.java index 2bdd5023978..35354e165cc 100644 --- a/framework/src/main/java/org/checkerframework/framework/qual/SubtypeOf.java +++ b/framework/src/main/java/org/checkerframework/framework/qual/SubtypeOf.java @@ -30,7 +30,8 @@ * public @interface MaybeAliased {} * * - *

        Together, all the @SubtypeOf meta-annotations fully describe the type qualifier hierarchy. + *

        Together, all the {@code @SubtypeOf} meta-annotations fully describe the type qualifier + * hierarchy. * * @checker_framework.manual #creating-declarative-hierarchy Declaratively defining the qualifier * hierarchy diff --git a/framework/src/main/java/org/checkerframework/framework/qual/UpperBoundFor.java b/framework/src/main/java/org/checkerframework/framework/qual/UpperBoundFor.java index 8e7e0bb9638..98aebcd7658 100644 --- a/framework/src/main/java/org/checkerframework/framework/qual/UpperBoundFor.java +++ b/framework/src/main/java/org/checkerframework/framework/qual/UpperBoundFor.java @@ -7,14 +7,24 @@ import java.lang.annotation.Target; /** - * A meta-annotation applied to the declaration of a type qualifier specifies that the given - * annotation should be upper bound for. + * A meta-annotation applied to the declaration of a type qualifier. It specifies that the + * annotation should be the upper bound for * *

          - *
        • a use of a particular type. - *
        • a use of a particular kind of type. + *
        • all uses of a particular type, and + *
        • all uses of a particular kind of type. *
        * + * An example is the declaration + * + *
        
        + * {@literal @}DefaultFor(classes=String.class)
        + * {@literal @}interface MyAnno {}
        + * 
        + * + *

        The upper bound applies to every occurrence of the given classes and also to every occurrence + * of the given type kinds. + * * @checker_framework.manual #upper-bound-for-use Upper bound of qualifiers on uses of a given type */ @Documented @@ -22,18 +32,18 @@ @Target(ElementType.ANNOTATION_TYPE) public @interface UpperBoundFor { /** - * Returns {@link TypeKind}s of types for which an annotation should be added by default. + * Returns {@link TypeKind}s of types that get an upper bound. The meta-annotated annotation is + * the upper bound. * - * @return {@link TypeKind}s of types for which an annotation should be added by default + * @return {@link TypeKind}s of types that get an upper bound */ TypeKind[] typeKinds() default {}; /** - * Returns {@link Class}es for which an annotation should be applied. For example, if - * {@code @MyAnno} is meta-annotated with {@code @DefaultFor(classes=String.class)}, then every - * occurrence of {@code String} is actually {@code @MyAnno String}. + * Returns {@link Class}es that should get an upper bound. The meta-annotated annotation is the + * upper bound. * - * @return {@link Class}es for which an annotation should be applied + * @return {@link Class}es that get an upper bound */ Class[] types() default {}; } diff --git a/framework/src/main/java/org/checkerframework/framework/source/DiagMessage.java b/framework/src/main/java/org/checkerframework/framework/source/DiagMessage.java index 8f8778421fe..5080bc7b988 100644 --- a/framework/src/main/java/org/checkerframework/framework/source/DiagMessage.java +++ b/framework/src/main/java/org/checkerframework/framework/source/DiagMessage.java @@ -9,6 +9,7 @@ import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.dataflow.qual.Pure; import org.checkerframework.dataflow.qual.SideEffectFree; +import org.checkerframework.framework.qual.AnnotatedFor; /** * A {@code DiagMessage} is a kind, a message key, and arguments. The message key will be expanded @@ -16,6 +17,7 @@ * *

        By contrast, {@code javax.tools.Diagnostic} has just a string message. */ +@AnnotatedFor("nullness") public class DiagMessage { /** The kind of message. */ private final Kind kind; @@ -31,6 +33,10 @@ public class DiagMessage { * @param messageKey the message key * @param args the arguments that will be interpolated into the localized message */ + @SuppressWarnings({ + "nullness:assignment.type.incompatible", // this call to Arrays.copyOf is polymorphic + "nullness:argument.type.incompatible" // https://tinyurl.com/cfissue/3448 + }) public DiagMessage(Kind kind, @CompilerMessageKey String messageKey, Object... args) { this.kind = kind; this.messageKey = messageKey; diff --git a/framework/src/main/java/org/checkerframework/framework/source/SourceChecker.java b/framework/src/main/java/org/checkerframework/framework/source/SourceChecker.java index 8c19e2e517d..e3e117c176f 100644 --- a/framework/src/main/java/org/checkerframework/framework/source/SourceChecker.java +++ b/framework/src/main/java/org/checkerframework/framework/source/SourceChecker.java @@ -52,8 +52,10 @@ import javax.lang.model.element.TypeElement; import javax.lang.model.util.Elements; import javax.lang.model.util.Types; +import javax.tools.Diagnostic; import javax.tools.Diagnostic.Kind; import org.checkerframework.checker.compilermsgs.qual.CompilerMessageKey; +import org.checkerframework.checker.interning.qual.InternedDistinct; import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.common.basetype.BaseTypeChecker; import org.checkerframework.framework.qual.AnnotatedFor; @@ -68,6 +70,7 @@ import org.checkerframework.javacutil.ElementUtils; import org.checkerframework.javacutil.SystemUtil; import org.checkerframework.javacutil.TreeUtils; +import org.checkerframework.javacutil.TypeSystemError; import org.checkerframework.javacutil.UserError; import org.plumelib.util.UtilPlume; @@ -251,6 +254,8 @@ /// Amount of detail in messages + // Print the version of the Checker Framework + "version", // Print info about git repository from which the Checker Framework was compiled "printGitProperties", @@ -313,6 +318,10 @@ // org.checkerframework.framework.util.typeinference.DefaultTypeArgumentInference "showInferenceSteps", + // Output a stack trace when reporting errors or warnings + // org.checkerframework.common.basetype.SourceChecker.printStackTrace() + "dumpOnErrors", + /// Visualizing the CFG // Implemented in the wrapper rather than this file, but worth noting here. @@ -398,13 +407,13 @@ public abstract class SourceChecker extends AbstractTypeProcessor protected Trees trees; /** The source tree that is being scanned. */ - protected CompilationUnitTree currentRoot; + protected @InternedDistinct CompilationUnitTree currentRoot; /** * If an error is detected in a CompilationUnitTree, skip all future calls of {@link * #typeProcess} with that same CompilationUnitTree. */ - private CompilationUnitTree previousErrorCompilationUnit; + private @InternedDistinct CompilationUnitTree previousErrorCompilationUnit; /** The visitor to use. */ protected SourceVisitor visitor; @@ -556,6 +565,7 @@ public SourceChecker getParentChecker() { * * @param newRoot the new compilation unit root */ + @SuppressWarnings("interning:assignment.type.incompatible") // used in == tests protected void setRoot(CompilationUnitTree newRoot) { this.currentRoot = newRoot; visitor.setRoot(currentRoot); @@ -778,8 +788,13 @@ public void run() { } }); } + if (hasOption("version")) { + messager.printMessage(Kind.NOTE, "Checker Framework " + getCheckerVersion()); + } } catch (UserError ce) { logUserError(ce); + } catch (TypeSystemError ce) { + logTypeSystemError(ce); } catch (BugInCF ce) { logBugInCF(ce); } catch (Throwable t) { @@ -852,7 +867,9 @@ public void typeProcess(TypeElement e, TreePath p) { Log log = Log.instance(context); if (log.nerrors > this.errsOnLastExit) { this.errsOnLastExit = log.nerrors; - previousErrorCompilationUnit = p.getCompilationUnit(); + @SuppressWarnings("interning:assignment.type.incompatible") // will be compared with == + @InternedDistinct CompilationUnitTree cu = p.getCompilationUnit(); + previousErrorCompilationUnit = cu; return; } if (p.getCompilationUnit() == previousErrorCompilationUnit) { @@ -895,6 +912,8 @@ public void typeProcess(TypeElement e, TreePath p) { warnUnneededSuppressions(); } catch (UserError ce) { logUserError(ce); + } catch (TypeSystemError ce) { + logTypeSystemError(ce); } catch (BugInCF ce) { logBugInCF(ce); } catch (Throwable t) { @@ -1059,7 +1078,7 @@ private void printMessage(String msg) { * * @param kind the kind of message to print * @param message the message text - * @param source the souce code position of the diagnostic message + * @param source the source code position of the diagnostic message * @param root the compilation unit */ protected void printOrStoreMessage( @@ -1067,7 +1086,44 @@ protected void printOrStoreMessage( String message, Tree source, CompilationUnitTree root) { + StackTraceElement[] trace = Thread.currentThread().getStackTrace(); + printOrStoreMessage(kind, message, source, root, trace); + } + + /** + * Stores all messages and sorts them by location before outputting them for compound checkers. + * This method is overloaded with an additional stack trace argument. The stack trace is printed + * when the dumpOnErrors option is enabled. + * + * @param kind the kind of message to print + * @param message the message text + * @param source the source code position of the diagnostic message + * @param root the compilation unit + * @param trace the stack trace where the checker encountered an error + */ + protected void printOrStoreMessage( + javax.tools.Diagnostic.Kind kind, + String message, + Tree source, + CompilationUnitTree root, + StackTraceElement[] trace) { Trees.instance(processingEnv).printMessage(kind, message, source, root); + printStackTrace(trace); + } + + /** + * Output the given stack trace if the "dumpOnErrors" option is enabled. + * + * @param trace stack trace when the checker encountered a warning/error + */ + private void printStackTrace(StackTraceElement[] trace) { + if (hasOption("dumpOnErrors")) { + StringBuilder msg = new StringBuilder(); + for (StackTraceElement elem : trace) { + msg.append("\tat " + elem + "\n"); + } + message(Diagnostic.Kind.NOTE, msg.toString()); + } } /////////////////////////////////////////////////////////////////////////// @@ -1478,35 +1534,35 @@ private Map createActiveOptions(Map options) { String[] split = key.split(OPTION_SEPARATOR); + splitlengthswitch: switch (split.length) { case 1: - // No separator, option always active + // No separator, option always active. activeOpts.put(key, value); break; case 2: - // Valid class-option pair Class clazz = this.getClass(); do { if (clazz.getCanonicalName().equals(split[0]) || clazz.getSimpleName().equals(split[0])) { + // Valid class-option pair. activeOpts.put(split[1], value); + break splitlengthswitch; } clazz = clazz.getSuperclass(); } while (clazz != null && !clazz.getName() .equals(AbstractTypeProcessor.class.getCanonicalName())); + // Didn't find a matching class. Option might be for another processor. Add + // option anyways. javac will warn if no processor supports the option. + activeOpts.put(key, value); break; default: - throw new UserError( - "Invalid option name: " - + key - + " At most one separator " - + OPTION_SEPARATOR - + " expected, but found " - + split.length - + "."); + // Too many separators. Option might be for another processor. Add option + // anyways. javac will warn if no processor supports the option. + activeOpts.put(key, value); } } return Collections.unmodifiableMap(activeOpts); @@ -2324,6 +2380,16 @@ private void logUserError(UserError ce) { printMessage(msg); } + /** + * Log (that is, print) a type system error. + * + * @param ce the type system error to output + */ + private void logTypeSystemError(TypeSystemError ce) { + String msg = ce.getMessage(); + printMessage(msg); + } + /** * Log (that is, print) an internal error in the framework or a checker. * @@ -2511,4 +2577,18 @@ private void printGitProperties() { System.out.println("IOException while reading git.properties: " + e.getMessage()); } } + + /** + * Returns the version of the Checker Framework. + * + * @return Checker Framework version + */ + private String getCheckerVersion() { + Properties gitProperties = getProperties(getClass(), "/git.properties"); + String version = gitProperties.getProperty("git.build.version"); + if (version != null) { + return version; + } + throw new BugInCF("Could not find the version in git.properties"); + } } diff --git a/framework/src/main/java/org/checkerframework/framework/stub/StubParser.java b/framework/src/main/java/org/checkerframework/framework/stub/StubParser.java index bad0dbd6026..e88591773b2 100644 --- a/framework/src/main/java/org/checkerframework/framework/stub/StubParser.java +++ b/framework/src/main/java/org/checkerframework/framework/stub/StubParser.java @@ -54,6 +54,7 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.StringJoiner; import javax.annotation.processing.ProcessingEnvironment; import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.Element; @@ -67,6 +68,7 @@ import javax.lang.model.type.TypeMirror; import javax.lang.model.util.ElementFilter; import javax.lang.model.util.Elements; +import javax.lang.model.util.Types; import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.framework.qual.FromStubFile; import org.checkerframework.framework.type.AnnotatedTypeFactory; @@ -83,21 +85,26 @@ import org.checkerframework.javacutil.ElementUtils; import org.checkerframework.javacutil.Pair; +// From an implementation perspective, this class represents a single stub file, notably its +// annotated types and its declaration annotations. From a client perspective, it has two static +// methods as described below in the Javadoc. /** - * Given a stub file, yields the annotated types in it and the declaration annotations in it. The - * main entry point is {@link StubParser#parse(String, InputStream, AnnotatedTypeFactory, - * ProcessingEnvironment, Map, Map)}, which side-effects its last two arguments. + * This class has two static methods. Each method parses a stub file and adds annotations to two + * maps passed as arguments. * - *

        The constructor acts in two parts. First, it calls the Stub Parser to parse a stub file. Then, - * it walks the Stub Parser's AST to create/collect types and declaration annotations. + *

        The main entry point is {@link StubParser#parse(String, InputStream, AnnotatedTypeFactory, + * ProcessingEnvironment, Map, Map)}, which side-effects its last two arguments. It operates in two + * steps. First, it calls the Stub Parser to parse a stub file. Then, it walks the Stub Parser's AST + * to create/collect types and declaration annotations. + * + *

        The other entry point is {@link #parseJdkFileAsStub}. */ public class StubParser { /** - * Whether to print warnings about types/members that were not found. The warning is about - * whether a class/field in the stub file is not found on the user's real classpath. Since the - * stub file may contain packages that are not on the classpath, this can be OK, so default to - * false. + * Whether to print warnings about types/members that were not found. The warning states that a + * class/field in the stub file is not found on the user's real classpath. Since the stub file + * may contain packages that are not on the classpath, this can be OK, so default to false. */ private final boolean warnIfNotFound; @@ -164,17 +171,20 @@ public class StubParser { // The following variables are stored in the StubParser because otherwise they would need to be // passed through everywhere, which would be verbose. - /** The type that is currently being parsed. */ - FqName parseState; + /** + * The name of the type that is currently being parsed. After processing a package declaration + * but before processing a type declaration, the type part of this may be null. + */ + private FqName typeName; /** Output variable: .... */ - Map atypes; + private final Map atypes; /** * Map from a name (actually declaration element string) to the set of declaration annotations * on it. */ - Map> declAnnos; + private final Map> declAnnos; /** The line separator. */ private static final String LINE_SEPARATOR = System.lineSeparator().intern(); @@ -189,9 +199,13 @@ public class StubParser { * @param filename name of stub file, used only for diagnostic messages * @param atypeFactory AnnotatedTypeFactory to use * @param processingEnv ProcessingEnvironment to use + * @param atypes annotated types from this stub file are added to this map + * @param declAnnos map from a name (actually declaration element string) to the set of + * declaration annotations on it. Declaration annotations from this stub file are added to + * this map. * @param isJdkAsStub whether or not the stub file is a part of the JDK */ - public StubParser( + private StubParser( String filename, AnnotatedTypeFactory atypeFactory, ProcessingEnvironment processingEnv, @@ -203,9 +217,8 @@ public StubParser( this.processingEnv = processingEnv; this.elements = processingEnv.getElementUtils(); - // TODO: this should use SourceChecker.getOptions() to allow - // setting these flags per checker. However, that doesn't seem very - // pressing here. + // TODO: This should use SourceChecker.getOptions() to allow + // setting these flags per checker. Map options = processingEnv.getOptions(); this.warnIfNotFound = options.containsKey("stubWarnIfNotFound"); this.warnIfNotFoundIgnoresClasses = options.containsKey("stubWarnIfNotFoundIgnoresClasses"); @@ -284,6 +297,8 @@ private static List getImportableMembers(TypeElement typeElement) { return result; } + // TODO: I'm not sure what "found" means. This method seems to collect only those that are + // imported, so it will miss ones whose fully-qualified name is used in the stub file. /** * Returns all annotations found in the stub file, as a value for {@link #allStubAnnotations}. * Note that this also modifies {@link #importedConstants} and {@link #importedTypes}. @@ -295,6 +310,7 @@ private static List getImportableMembers(TypeElement typeElement) { private Map getAllStubAnnotations() { Map result = new HashMap<>(); + // TODO: The size can be greater than 1, but this ignores all but the first element. assert !stubUnit.getCompilationUnits().isEmpty(); CompilationUnit cu = stubUnit.getCompilationUnits().get(0); @@ -402,8 +418,10 @@ private void addEnclosingTypesToImportedTypes(Element element) { * @param inputStream of stub file to parse * @param atypeFactory AnnotatedTypeFactory to use * @param processingEnv ProcessingEnvironment to use - * @param atypes annotated types from this stub file is added to this map - * @param declAnnos declaration annotations from this stub file are added to this map + * @param atypes annotated types from this stub file are added to this map + * @param declAnnos map from a name (actually declaration element string) to the set of + * declaration annotations on it. Declaration annotations from this stub file are added to + * this map. */ public static void parse( String filename, @@ -423,8 +441,10 @@ public static void parse( * @param inputStream of stub file to parse * @param atypeFactory AnnotatedTypeFactory to use * @param processingEnv ProcessingEnvironment to use - * @param atypes annotated types from this stub file is added to this map - * @param declAnnos declaration annotations from this stub file are added to this map + * @param atypes annotated types from this stub file are added to this map + * @param declAnnos map from a name (actually declaration element string) to the set of + * declaration annotations on it. Declaration annotations from this stub file are added to + * this map. */ public static void parseJdkFileAsStub( String filename, @@ -443,8 +463,10 @@ public static void parseJdkFileAsStub( * @param inputStream of stub file to parse * @param atypeFactory AnnotatedTypeFactory to use * @param processingEnv ProcessingEnvironment to use - * @param atypes annotated types from this stub file is added to this map - * @param declAnnos declaration annotations from this stub file are added to this map + * @param atypes annotated types from this stub file are added to this map + * @param declAnnos map from a name (actually declaration element string) to the set of + * declaration annotations on it. Declaration annotations from this stub file are added to + * this map. * @param isJdkAsStub whether or not the stub file is a part of the annotated jdk */ private static void parse( @@ -462,23 +484,22 @@ private static void parse( sp.parseStubUnit(inputStream); sp.process(); } catch (ParseProblemException e) { - StringBuilder message = - new StringBuilder( - "exception while parsing stub file " - + filename - + ". Encountered problems: "); + StringJoiner message = new StringJoiner(LINE_SEPARATOR); + message.add( + e.getProblems().size() + " problems while parsing stub file " + filename + ":"); // Manually build up the message, to get verbose location information. for (Problem p : e.getProblems()) { - message.append(p.getVerboseMessage()); - message.append(LINE_SEPARATOR); + message.add(p.getVerboseMessage()); } sp.stubWarn(message.toString()); } } /** - * Delegate to the Stub Parser to parse the stub file to an AST. Subsequently, all work uses the - * AST. + * Delegate to the Stub Parser to parse the stub file to an AST, and save it in {@link + * #stubUnit}. Subsequently, all work uses the AST. + * + * @param inputStream the stream from which to read a stub file */ private void parseStubUnit(InputStream inputStream) { if (debugStubParser) { @@ -492,9 +513,10 @@ private void parseStubUnit(InputStream inputStream) { if (allStubAnnotations.isEmpty()) { stubWarnNotFound( String.format( - "No supported annotations found! This likely means stub file %s doesn't import them correctly.", + "No supported annotations found! Does stub file %s import them?", filename)); } + // Annotations in java.lang might be used without an import statement, so add them in case. allStubAnnotations.putAll(annosInPackage(findPackage("java.lang"))); } @@ -503,7 +525,11 @@ private void process() { processStubUnit(this.stubUnit); } - /** Parse the given StubUnit. */ + /** + * Process the given StubUnit. + * + * @param index the StubUnit to process + */ private void processStubUnit(StubUnit index) { for (CompilationUnit cu : index.getCompilationUnits()) { processCompilationUnit(cu); @@ -515,7 +541,7 @@ private void processCompilationUnit(CompilationUnit cu) { if (!cu.getPackageDeclaration().isPresent()) { packageAnnos = null; - parseState = new FqName(null, null); + typeName = new FqName(null, null); } else { PackageDeclaration pDecl = cu.getPackageDeclaration().get(); packageAnnos = pDecl.getAnnotations(); @@ -531,7 +557,7 @@ private void processCompilationUnit(CompilationUnit cu) { private void processPackage(PackageDeclaration packDecl) { assert (packDecl != null); String packageName = packDecl.getNameAsString(); - parseState = new FqName(packageName, null); + typeName = new FqName(packageName, null); Element elem = elements.getPackageElement(packageName); // If the element lookup fails, it's because we have an annotation for a // package that isn't on the classpath, which is fine. @@ -542,21 +568,25 @@ private void processPackage(PackageDeclaration packDecl) { } /** + * Process a type declaration + * + * @param typeDecl the type declaration to process * @param outertypeName the name of the containing class, when processing a nested class; * otherwise null + * @param packageAnnos the annotation declared in the package */ private void processTypeDecl( TypeDeclaration typeDecl, String outertypeName, List packageAnnos) { - assert parseState != null; + assert typeName != null; if (isJdkAsStub && typeDecl.getModifiers().contains(Modifier.privateModifier())) { - // Don't process private classes of the jdk. They can't be referenced outside of the - // jdk and might refer to types that are not accessible. + // Don't process private classes of the JDK. They can't be referenced outside of the + // JDK and might refer to types that are not accessible. return; } String innerName = (outertypeName == null ? "" : outertypeName + ".") + typeDecl.getNameAsString(); - parseState = new FqName(parseState.packageName, innerName); - String fqTypeName = parseState.toString(); + typeName = new FqName(typeName.packageName, innerName); + String fqTypeName = typeName.toString(); TypeElement typeElt = elements.getTypeElement(fqTypeName); if (typeElt == null) { if (debugStubParser @@ -615,7 +645,7 @@ private boolean hasNoStubParserWarning(Iterable aexprs) { return false; } for (AnnotationExpr anno : aexprs) { - if (anno.getNameAsString().contentEquals("NoStubParserWarning")) { + if (anno.getNameAsString().equals("NoStubParserWarning")) { return true; } } @@ -623,9 +653,11 @@ private boolean hasNoStubParserWarning(Iterable aexprs) { } /** - * Returns list of AnnotatedTypeVariable of the type's type parameter declarations. + * Returns the type's type parameter declarations. * - * @return list of AnnotatedTypeVariable of the type's type parameter declarations + * @param decl a type declaration + * @param elt the type's element + * @return the type's type parameter declarations */ private List processType( ClassOrInterfaceDeclaration decl, TypeElement elt) { @@ -648,7 +680,7 @@ private List processType( if (numParams != numArgs) { stubDebug( String.format( - "parseType: mismatched sizes for typeParameters=%s (size %d) and typeArguments=%s (size %d); decl=%s; elt=%s (%s); type=%s (%s); parseState=%s", + "parseType: mismatched sizes for typeParameters=%s (size %d) and typeArguments=%s (size %d); decl=%s; elt=%s (%s); type=%s (%s); typeName=%s", typeParameters, numParams, typeArguments, @@ -658,7 +690,7 @@ private List processType( elt.getClass(), type, type.getClass(), - parseState)); + typeName)); stubDebug("Proceeding despite mismatched sizes"); } } @@ -682,12 +714,11 @@ private List processType( } /** - * Gathers and returns a list of AnnotatedTypeVariable of the enum's type parameter - * declarations. + * Returns an enum's type parameter declarations. * - * @param decl actual enum declaration + * @param decl enum declaration * @param elt element representing enum - * @return list of AnnotatedTypeVariable of the enum's type parameter declarations + * @return the enum's type parameter declarations */ private List processEnum(EnumDeclaration decl, TypeElement elt) { @@ -776,6 +807,7 @@ private void processCallableDeclaration(CallableDeclaration decl, ExecutableE ((MethodDeclaration) decl).getType(), decl.getAnnotations()); } else { + assert decl.isConstructorDeclaration(); annotate(methodType.getReturnType(), decl.getAnnotations()); } @@ -850,7 +882,7 @@ private void processParameters( if (param.isVarArgs()) { assert paramType.getKind() == TypeKind.ARRAY; // The "type" of param is actually the component type of the vararg. - // For example, "Object..." the type would be "Object". + // For example, in "Object..." the type would be "Object". annotate( ((AnnotatedArrayType) paramType).getComponentType(), param.getType(), @@ -899,7 +931,7 @@ private void clearAnnotations(AnnotatedTypeMirror atype, Type typeDef) { /** * Add the annotations from {@code type} to {@code atype}. Type annotations that parsed as - * declaration annotations (i.e., type annotations in {@code declAnnos} are applied to the + * declaration annotations (i.e., type annotations in {@code declAnnos}) are applied to the * innermost component type. * * @param atype annotated type to which to add annotations @@ -907,11 +939,18 @@ private void clearAnnotations(AnnotatedTypeMirror atype, Type typeDef) { * @param declAnnos annotations stored on the declaration of the variable with this type or null */ private void annotateAsArray( - AnnotatedArrayType atype, ReferenceType type, NodeList declAnnos) { + AnnotatedArrayType atype, + ReferenceType type, + @Nullable NodeList declAnnos) { annotateInnermostComponentType(atype, declAnnos); Type typeDef = type; AnnotatedTypeMirror currentAtype = atype; - while (typeDef.isArrayType() && currentAtype.getKind() == TypeKind.ARRAY) { + while (typeDef.isArrayType()) { + if (currentAtype.getKind() != TypeKind.ARRAY) { + stubWarn("Mismatched array lengths; atype: " + atype + "%n type: " + type); + return; + } + // handle generic type clearAnnotations(currentAtype, typeDef); @@ -921,9 +960,9 @@ private void annotateAsArray( } typeDef = ((com.github.javaparser.ast.type.ArrayType) typeDef).getComponentType(); currentAtype = ((AnnotatedArrayType) currentAtype).getComponentType(); - if (typeDef.isArrayType() ^ currentAtype.getKind() == TypeKind.ARRAY) { - stubWarn("Mismatched array lengths; atype: " + atype + "%n type: " + type); - } + } + if (currentAtype.getKind() == TypeKind.ARRAY) { + stubWarn("Mismatched array lengths; atype: " + atype + "%n type: " + type); } } @@ -952,7 +991,7 @@ private ClassOrInterfaceType unwrapDeclaredType(Type type) { * null */ private void annotate( - AnnotatedTypeMirror atype, Type typeDef, NodeList declAnnos) { + AnnotatedTypeMirror atype, Type typeDef, @Nullable NodeList declAnnos) { if (atype.getKind() == TypeKind.ARRAY) { if (typeDef instanceof ReferenceType) { annotateAsArray((AnnotatedArrayType) atype, (ReferenceType) typeDef, declAnnos); @@ -1025,7 +1064,7 @@ private void annotate( + typeDef + ">" + " while parsing " - + parseState); + + typeName); return; } WildcardType wildcardDef = (WildcardType) typeDef; @@ -1045,8 +1084,10 @@ private void annotate( case TYPEVAR: // Add annotations from the declaration of the TypeVariable AnnotatedTypeVariable typeVarUse = (AnnotatedTypeVariable) atype; + Types typeUtils = processingEnv.getTypeUtils(); for (AnnotatedTypeVariable typePar : typeParameters) { - if (typePar.getUnderlyingType() == atype.getUnderlyingType()) { + if (typeUtils.isSameType( + typePar.getUnderlyingType(), atype.getUnderlyingType())) { AnnotatedTypeReplacer.replace( typePar.getUpperBound(), typeVarUse.getUpperBound()); AnnotatedTypeReplacer.replace( @@ -1081,14 +1122,14 @@ private boolean mightHaveTypeArguments(AnnotatedTypeMirror atype) { /** * Process the field declaration in decl, and attach any type qualifiers to the type of elt in - * {@link #atypes} + * {@link #atypes}. * * @param decl the declaration in the stub file * @param elt the element representing that same declaration */ private void processField(FieldDeclaration decl, VariableElement elt) { if (isJdkAsStub && decl.getModifiers().contains(Modifier.privateModifier())) { - // Don't process private fields of the jdk. They can't be referenced outside of the jdk + // Don't process private fields of the JDK. They can't be referenced outside of the JDK // and might refer to types that are not accessible. return; } @@ -1195,8 +1236,8 @@ private void recordDeclAnnotation(Element elt, List annotations) } } } - String key = ElementUtils.getVerboseName(elt); - putOrAddToMap(declAnnos, key, annos); + String eltName = ElementUtils.getVerboseName(elt); + putOrAddToMap(declAnnos, eltName, annos); } /** @@ -1362,7 +1403,7 @@ private AnnotatedDeclaredType findType( * @return nested in typeElt element with the name of the class or interface or null if nested * element is not found */ - private Element findElement(TypeElement typeElt, ClassOrInterfaceDeclaration ciDecl) { + private @Nullable Element findElement(TypeElement typeElt, ClassOrInterfaceDeclaration ciDecl) { final String wantedClassOrInterfaceName = ciDecl.getNameAsString(); for (TypeElement typeElement : ElementUtils.getAllTypeElementsIn(typeElt)) { if (wantedClassOrInterfaceName.equals(typeElement.getSimpleName().toString())) { @@ -1391,7 +1432,7 @@ private Element findElement(TypeElement typeElt, ClassOrInterfaceDeclaration ciD * @return nested in typeElt enum element with the name of the provided enum or null if nested * element is not found */ - private Element findElement(TypeElement typeElt, EnumDeclaration enumDecl) { + private @Nullable Element findElement(TypeElement typeElt, EnumDeclaration enumDecl) { final String wantedEnumName = enumDecl.getNameAsString(); for (TypeElement typeElement : ElementUtils.getAllTypeElementsIn(typeElt)) { if (wantedEnumName.equals(typeElement.getSimpleName().toString())) { @@ -1418,7 +1459,7 @@ private Element findElement(TypeElement typeElt, EnumDeclaration enumDecl) { * @return enum constant element in typeElt with the provided name or null if enum constant * element is not found */ - private VariableElement findElement( + private @Nullable VariableElement findElement( TypeElement typeElt, EnumConstantDeclaration enumConstDecl) { final String enumConstName = enumConstDecl.getNameAsString(); return findFieldElement(typeElt, enumConstName); @@ -1435,10 +1476,11 @@ private VariableElement findElement( * @return method element in typeElt with the same signature as the provided method declaration * or null if method element is not found */ - private ExecutableElement findElement(TypeElement typeElt, MethodDeclaration methodDecl) { + private @Nullable ExecutableElement findElement( + TypeElement typeElt, MethodDeclaration methodDecl) { if (isJdkAsStub && methodDecl.getModifiers().contains(Modifier.privateModifier())) { - // Don't process private methods of the jdk. They can't be referenced outside of the - // jdk and might refer to types that are not accessible. + // Don't process private methods of the JDK. They can't be referenced outside of the + // JDK and might refer to types that are not accessible. return null; } final String wantedMethodName = methodDecl.getNameAsString(); @@ -1485,11 +1527,11 @@ private ExecutableElement findElement(TypeElement typeElt, MethodDeclaration met * @return constructor element in typeElt with the same signature as the provided constructor * declaration or null if constructor element is not found */ - private ExecutableElement findElement( + private @Nullable ExecutableElement findElement( TypeElement typeElt, ConstructorDeclaration constructorDecl) { if (isJdkAsStub && constructorDecl.getModifiers().contains(Modifier.privateModifier())) { - // Don't process private constructors of the jdk. They can't be referenced outside of - // the jdk and might refer to types that are not accessible. + // Don't process private constructors of the JDK. They can't be referenced outside of + // the JDK and might refer to types that are not accessible. return null; } final int wantedMethodParams = @@ -1529,7 +1571,7 @@ private VariableElement findElement(TypeElement typeElt, VariableDeclarator vari * @param fieldName field name that should be found * @return field element in typeElt with the provided name or null if field element is not found */ - private VariableElement findFieldElement(TypeElement typeElt, String fieldName) { + private @Nullable VariableElement findFieldElement(TypeElement typeElt, String fieldName) { for (VariableElement field : ElementUtils.getAllFieldsIn(typeElt, elements)) { // field.getSimpleName() is a CharSequence, not a String if (fieldName.equals(field.getSimpleName().toString())) { @@ -1549,8 +1591,11 @@ private VariableElement findFieldElement(TypeElement typeElt, String fieldName) /** * Given a fully-qualified type name, return a TypeElement for it, or null if none exists. Also * cache in importedTypes. + * + * @param name a fully-qualified type name + * @return a TypeElement for the name, or null */ - private TypeElement getTypeElementOrNull(String name) { + private @Nullable TypeElement getTypeElementOrNull(String name) { TypeElement typeElement = elements.getTypeElement(name); if (typeElement != null) { importedTypes.put(name, typeElement); @@ -1639,8 +1684,15 @@ private AnnotationMirror getAnnotation( } } - /** Returns the value of {@code expr}, or null if some problem occurred getting the value. */ - private Object getValueOfExpressionInAnnotation( + /** + * Returns the value of {@code expr}, or null if some problem occurred getting the value. + * + * @param name the name of an annotation element/argument, used for diagnostic messages + * @param expr the expression to determine the value of + * @param valueKind the type of the result + * @return the value of {@code expr}, or null if some problem occurred getting the value + */ + private @Nullable Object getValueOfExpressionInAnnotation( String name, Expression expr, TypeKind valueKind) { if (expr instanceof FieldAccessExpr || expr instanceof NameExpr) { VariableElement elem; @@ -1719,23 +1771,23 @@ private Object getValueOfExpressionInAnnotation( /** * Returns the TypeElement with the name {@code name}, if one exists. Otherwise, checks the - * class and package of {@code parseState} for a class named {@code name}. + * class and package of {@code typeName} for a class named {@code name}. * * @param name classname (simple, or Outer.Inner, or fully-qualified) * @return the TypeElement for {@code name}, or null if not found */ private @Nullable TypeElement findTypeOfName(String name) { - String packageName = parseState.packageName; + String packageName = typeName.packageName; String packagePrefix = (packageName == null) ? "" : packageName + "."; - // stubWarn("findTypeOfName(%s), parseState %s %s", name, packageName, enclosingClass); + // stubWarn("findTypeOfName(%s), typeName %s %s", name, packageName, enclosingClass); // As soon as typeElement is set to a non-null value, it will be returned. TypeElement typeElement = getTypeElementOrNull(name); if (typeElement == null && packageName != null) { typeElement = getTypeElementOrNull(packagePrefix + name); } - String enclosingClass = parseState.className; + String enclosingClass = typeName.className; while (typeElement == null && enclosingClass != null) { typeElement = getTypeElementOrNull(packagePrefix + enclosingClass + "." + name); int lastDot = enclosingClass.lastIndexOf('.'); @@ -2024,7 +2076,7 @@ private void putMerge( if (m.containsKey(key)) { AnnotatedTypeMirror existingType = m.get(key); // If the newType is from a JDK stub file, then keep the existing type. This - // way user supplied stub files override jdk stub files. + // way user supplied stub files override JDK stub files. if (!isJdkAsStub) { AnnotatedTypeReplacer.replace(newType, existingType); } @@ -2110,11 +2162,21 @@ private void stubDebug(String warning) { /** Represents a class: its package name and simple name. */ private static class FqName { /** Name of the package being parsed, or null. */ - public String packageName; - - /** Name of the type being parsed. Includes outer class names if any. */ - public String className; - + public @Nullable String packageName; + + /** + * Name of the type being parsed. Includes outer class names if any. Null if the parser has + * parsed a package declaration but has not yet gotten to a type declaration. + */ + public @Nullable String className; + + /** + * Create a new FqName, which represents a class. + * + * @param packageName name of the package, or null + * @param className unqualified name of the type, including outer class names if any. May be + * null. + */ public FqName(String packageName, String className) { this.packageName = packageName; this.className = className; diff --git a/framework/src/main/java/org/checkerframework/framework/type/AnnotatedTypeFactory.java b/framework/src/main/java/org/checkerframework/framework/type/AnnotatedTypeFactory.java index f9e85d7bade..09d98182d43 100644 --- a/framework/src/main/java/org/checkerframework/framework/type/AnnotatedTypeFactory.java +++ b/framework/src/main/java/org/checkerframework/framework/type/AnnotatedTypeFactory.java @@ -56,6 +56,8 @@ import javax.lang.model.type.WildcardType; import javax.lang.model.util.Elements; import javax.lang.model.util.Types; +import org.checkerframework.checker.interning.qual.FindDistinct; +import org.checkerframework.checker.interning.qual.InternedDistinct; import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.common.basetype.BaseTypeChecker; import org.checkerframework.common.basetype.BaseTypeVisitor; @@ -104,6 +106,7 @@ import org.checkerframework.javacutil.ElementUtils; import org.checkerframework.javacutil.Pair; import org.checkerframework.javacutil.TreeUtils; +import org.checkerframework.javacutil.TypeSystemError; import org.checkerframework.javacutil.TypesUtils; import org.checkerframework.javacutil.UserError; import org.checkerframework.javacutil.trees.DetachedVarSymbol; @@ -488,9 +491,7 @@ public AnnotatedTypeFactory(BaseTypeChecker checker) { */ private void checkSupportedQuals() { if (supportedQuals.isEmpty()) { - // This is throwing a CF bug, but it could also be a bug in the checker rather than in - // the framework itself. - throw new BugInCF("Found no supported qualifiers."); + throw new TypeSystemError("Found no supported qualifiers."); } for (Class annotationClass : supportedQuals) { // Check @Target values @@ -519,7 +520,7 @@ private void checkSupportedQuals() { buf.append(otherElementTypes.get(i)); } buf.append("."); - throw new BugInCF(buf.toString()); + throw new TypeSystemError(buf.toString()); } } } @@ -550,7 +551,8 @@ protected void checkInvalidOptionsInferSignatures() { protected void postInit() { this.qualHierarchy = createQualifierHierarchy(); if (qualHierarchy == null) { - throw new BugInCF("AnnotatedTypeFactory with null qualifier hierarchy not supported."); + throw new TypeSystemError( + "AnnotatedTypeFactory with null qualifier hierarchy not supported."); } this.typeHierarchy = createTypeHierarchy(); this.typeVarSubstitutor = createTypeVariableSubstitutor(); @@ -716,7 +718,7 @@ protected static QualifierHierarchy createQualifierHierarchy( if (typeQualifier.getAnnotation(SubtypeOf.class) != null) { // This is currently not supported. At some point we might add // polymorphic qualifiers with upper and lower bounds. - throw new BugInCF( + throw new TypeSystemError( "AnnotatedTypeFactory: " + typeQualifier + " is polymorphic and specifies super qualifiers. " @@ -725,7 +727,7 @@ protected static QualifierHierarchy createQualifierHierarchy( continue; } if (typeQualifier.getAnnotation(SubtypeOf.class) == null) { - throw new BugInCF( + throw new TypeSystemError( "AnnotatedTypeFactory: %s does not specify its super qualifiers.%n" + "Add an @org.checkerframework.framework.qual.SubtypeOf annotation to it,%n" + "or if it is an alias, exclude it from `createSupportedTypeQualifiers()`.%n", @@ -735,14 +737,14 @@ protected static QualifierHierarchy createQualifierHierarchy( typeQualifier.getAnnotation(SubtypeOf.class).value(); for (Class superQualifier : superQualifiers) { if (!supportedTypeQualifiers.contains(superQualifier)) { - throw new BugInCF( + throw new TypeSystemError( "Found unsupported qualifier in SubTypeOf: %s on qualifier: %s", superQualifier.getCanonicalName(), typeQualifier.getCanonicalName()); } if (superQualifier.getAnnotation(PolymorphicQualifier.class) != null) { // This is currently not supported. No qualifier can have a polymorphic // qualifier as super qualifier. - throw new BugInCF( + throw new TypeSystemError( "Found polymorphic qualifier in SubTypeOf: %s on qualifier: %s", superQualifier.getCanonicalName(), typeQualifier.getCanonicalName()); } @@ -754,7 +756,7 @@ protected static QualifierHierarchy createQualifierHierarchy( QualifierHierarchy hierarchy = factory.build(); if (!hierarchy.isValid()) { - throw new BugInCF( + throw new TypeSystemError( "AnnotatedTypeFactory: invalid qualifier hierarchy: " + hierarchy.getClass() + " " @@ -777,10 +779,10 @@ public final QualifierHierarchy getQualifierHierarchy() { } /** - * Creates the type subtyping checker using the current type qualifier hierarchy. + * Creates the type hierarchy to be used by this factory. * *

        Subclasses may override this method to specify new type-checking rules beyond the typical - * java subtyping rules. + * Java subtyping rules. * * @return the type relations class to check type subtyping */ @@ -1304,7 +1306,7 @@ private AnnotatedTypeMirror mergeStubsIntoType(@Nullable AnnotatedTypeMirror typ * @param elt the element from which to read stub types * @return the type, side-effected to add the stub types */ - private AnnotatedTypeMirror mergeStubsIntoType( + protected AnnotatedTypeMirror mergeStubsIntoType( @Nullable AnnotatedTypeMirror type, Element elt) { AnnotatedTypeMirror stubType = stubTypes.getAnnotatedTypeMirror(elt); if (stubType != null) { @@ -2027,8 +2029,8 @@ public ParameterizedExecutableType( *

        The return type is a pair of the type of the invoked method and the (inferred) type * arguments. Note that neither the explicitly passed nor the inferred type arguments are * guaranteed to be subtypes of the corresponding upper bounds. See method {@link - * org.checkerframework.common.basetype.BaseTypeVisitor#checkTypeArguments(Tree, List, List, - * List)} for the checks of type argument well-formedness. + * org.checkerframework.common.basetype.BaseTypeVisitor#checkTypeArguments} for the checks of + * type argument well-formedness. * *

        Note that "this" and "super" constructor invocations are also handled by this method * (explicit or implicit ones, at the beginning of a constructor). Method {@link @@ -2208,8 +2210,8 @@ protected void adaptGetClassReturnTypeToReceiver( *

        The return type is a pair of the type of the invoked constructor and the (inferred) type * arguments. Note that neither the explicitly passed nor the inferred type arguments are * guaranteed to be subtypes of the corresponding upper bounds. See method {@link - * org.checkerframework.common.basetype.BaseTypeVisitor#checkTypeArguments(Tree, List, List, - * List)} for the checks of type argument well-formedness. + * org.checkerframework.common.basetype.BaseTypeVisitor#checkTypeArguments} for the checks of + * type argument well-formedness. * *

        Note that "this" and "super" constructor invocations are handled by method {@link * #methodFromUse}. This method only handles constructor invocations in a "new" expression. @@ -2441,10 +2443,10 @@ public AnnotatedDeclaredType getBoxedType(AnnotatedPrimitiveType type) { } /** - * returns the annotated primitive type of the given declared type if it is a boxed declared + * Returns the annotated primitive type of the given declared type if it is a boxed declared * type. Otherwise, it throws IllegalArgumentException exception. * - *

        The returned type would have the annotations on the given type and nothing else. + *

        The returned type has the same primary annotations as the given type. * * @param type the declared type * @return the unboxed primitive type @@ -2926,7 +2928,8 @@ protected final AnnotatedDeclaredType getCurrentClassType(Tree tree) { if (res == null) { TreePath path = getPath(tree); if (path != null) { - MethodTree enclosingMethod = TreeUtils.enclosingMethod(path); + @SuppressWarnings("interning:assignment.type.incompatible") // used for == test + @InternedDistinct MethodTree enclosingMethod = TreeUtils.enclosingMethod(path); ClassTree enclosingClass = TreeUtils.enclosingClass(path); boolean found = false; @@ -3001,7 +3004,7 @@ private final Element getMostInnerClassOrMethod(Tree tree) { * @param node the {@link Tree} to get the path for * @return the path for {@code node} under the current root */ - public final TreePath getPath(Tree node) { + public final TreePath getPath(@FindDistinct Tree node) { assert root != null : "AnnotatedTypeFactory.getPath: root needs to be set when used on trees; factory: " + this.getClass(); diff --git a/framework/src/main/java/org/checkerframework/framework/type/AnnotatedTypeMirror.java b/framework/src/main/java/org/checkerframework/framework/type/AnnotatedTypeMirror.java index dc5c7974ff0..b7dd0501055 100644 --- a/framework/src/main/java/org/checkerframework/framework/type/AnnotatedTypeMirror.java +++ b/framework/src/main/java/org/checkerframework/framework/type/AnnotatedTypeMirror.java @@ -439,8 +439,9 @@ public boolean hasEffectiveAnnotation(AnnotationMirror a) { /** * Determines whether this type contains the given annotation explicitly written at declaration. - * This method considers the annotation's values, that is, if the type is "@A("s") @B(3) Object" - * a call with "@A("t") or "@A" will return false, whereas a call with "@B(3)" will return true. + * This method considers the annotation's values, that is, if the type is {@code @A("s") @B(3) + * Object}, a call with {@code @A("t")} or {@code @A} will return false, whereas a call with + * {@code @B(3)} will return true. * *

        In contrast to {@link #hasExplicitAnnotationRelaxed(AnnotationMirror)} this method also * compares annotation values. @@ -2095,7 +2096,7 @@ public static class AnnotatedUnionType extends AnnotatedTypeMirror { * Creates a new AnnotatedUnionType. * * @param type underlying kind of this type - * @param atypeFactory TODO + * @param atypeFactory type factory */ private AnnotatedUnionType(UnionType type, AnnotatedTypeFactory atypeFactory) { super(type, atypeFactory); diff --git a/framework/src/main/java/org/checkerframework/framework/type/AnnotatedTypeReplacer.java b/framework/src/main/java/org/checkerframework/framework/type/AnnotatedTypeReplacer.java index 59cf98351ec..3b5577e2013 100644 --- a/framework/src/main/java/org/checkerframework/framework/type/AnnotatedTypeReplacer.java +++ b/framework/src/main/java/org/checkerframework/framework/type/AnnotatedTypeReplacer.java @@ -30,6 +30,7 @@ public class AnnotatedTypeReplacer extends AnnotatedTypeComparer { * @param from the annotated type mirror from which to take new annotations * @param to the annotated type mirror to which the annotations will be added */ + @SuppressWarnings("interning:not.interned") // assertion public static void replace(final AnnotatedTypeMirror from, final AnnotatedTypeMirror to) { if (from == to) { throw new BugInCF("From == to"); @@ -46,6 +47,7 @@ public static void replace(final AnnotatedTypeMirror from, final AnnotatedTypeMi * @param to the annotated type mirror to which the annotations will be added * @param top the top type of the hierarchy whose annotations will be added */ + @SuppressWarnings("interning:not.interned") // assertion public static void replace( final AnnotatedTypeMirror from, final AnnotatedTypeMirror to, @@ -74,6 +76,7 @@ public AnnotatedTypeReplacer(final AnnotationMirror top) { this.top = top; } + @SuppressWarnings("interning:not.interned") // assertion @Override protected Void compare(AnnotatedTypeMirror from, AnnotatedTypeMirror to) { assert from != to; diff --git a/framework/src/main/java/org/checkerframework/framework/type/AsSuperVisitor.java b/framework/src/main/java/org/checkerframework/framework/type/AsSuperVisitor.java index f9e1554847a..2b97ccb7730 100644 --- a/framework/src/main/java/org/checkerframework/framework/type/AsSuperVisitor.java +++ b/framework/src/main/java/org/checkerframework/framework/type/AsSuperVisitor.java @@ -8,6 +8,7 @@ import javax.lang.model.type.TypeKind; import javax.lang.model.type.TypeMirror; import javax.lang.model.util.Types; +import org.checkerframework.checker.interning.qual.FindDistinct; import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedArrayType; import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedDeclaredType; import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedIntersectionType; @@ -33,8 +34,13 @@ public class AsSuperVisitor extends AbstractAtmComboVisitor the type of the supertype * @param type type from which to copy annotations * @param superType a type whose erased Java type is a supertype of {@code type}'s erased Java * type. * @return a copy of {@code superType} with annotations copied from {@code type} and type * variables substituted from {@code type}. */ - @SuppressWarnings("unchecked") + @SuppressWarnings({ + "unchecked", + "interning:not.interned" // optimized special case + }) public T asSuper(AnnotatedTypeMirror type, T superType) { if (type == null || superType == null) { throw new BugInCF("AsSuperVisitor type and supertype cannot be null."); + } - } else if (type == superType) { + if (type == superType) { return (T) type.deepCopy(); } @@ -75,7 +86,7 @@ public T asSuper(AnnotatedTypeMirror type, T sup } private void reset() { - isUninferredTypeAgrument = false; + isUninferredTypeArgument = false; } @Override @@ -148,7 +159,7 @@ private AnnotatedTypeMirror errorTypeNotErasedSubtypeOfSuperType( // Any type can be converted to a String return visit(annotatedTypeFactory.getStringType(type), superType, p); } - if (isUninferredTypeAgrument) { + if (isUninferredTypeArgument) { return copyPrimaryAnnos(type, superType); } throw new BugInCF( @@ -721,12 +732,12 @@ public AnnotatedTypeMirror visitUnion_Wildcard( private AnnotatedTypeMirror visitWildcard_NotTypvarNorWildcard( AnnotatedWildcardType type, AnnotatedTypeMirror superType, Void p) { - boolean oldIsUninferredTypeArgument = isUninferredTypeAgrument; + boolean oldIsUninferredTypeArgument = isUninferredTypeArgument; if (type.isUninferredTypeArgument()) { - isUninferredTypeAgrument = true; + isUninferredTypeArgument = true; } AnnotatedTypeMirror asSuper = visit(type.getExtendsBound(), superType, p); - isUninferredTypeAgrument = oldIsUninferredTypeArgument; + isUninferredTypeArgument = oldIsUninferredTypeArgument; annotatedTypeFactory.addDefaultAnnotations(superType); return copyPrimaryAnnos(type, asSuper); @@ -759,9 +770,9 @@ public AnnotatedTypeMirror visitWildcard_Primitive( @Override public AnnotatedTypeMirror visitWildcard_Typevar( AnnotatedWildcardType type, AnnotatedTypeVariable superType, Void p) { - boolean oldIsUninferredTypeArgument = isUninferredTypeAgrument; + boolean oldIsUninferredTypeArgument = isUninferredTypeArgument; if (type.isUninferredTypeArgument()) { - isUninferredTypeAgrument = true; + isUninferredTypeArgument = true; } AnnotatedTypeMirror upperBound = visit(type.getExtendsBound(), superType.getUpperBound(), p); @@ -777,7 +788,7 @@ public AnnotatedTypeMirror visitWildcard_Typevar( lowerBound = asSuperTypevarLowerBound(type.getSuperBound(), superType, p); } superType.setLowerBound(lowerBound); - isUninferredTypeAgrument = oldIsUninferredTypeArgument; + isUninferredTypeArgument = oldIsUninferredTypeArgument; annotatedTypeFactory.addDefaultAnnotations(superType); return copyPrimaryAnnos(type, superType); @@ -792,9 +803,9 @@ public AnnotatedTypeMirror visitWildcard_Union( @Override public AnnotatedTypeMirror visitWildcard_Wildcard( AnnotatedWildcardType type, AnnotatedWildcardType superType, Void p) { - boolean oldIsUninferredTypeArgument = isUninferredTypeAgrument; + boolean oldIsUninferredTypeArgument = isUninferredTypeArgument; if (type.isUninferredTypeArgument()) { - isUninferredTypeAgrument = true; + isUninferredTypeArgument = true; superType.setUninferredTypeArgument(); } if (types.isSubtype( @@ -828,13 +839,20 @@ public AnnotatedTypeMirror visitWildcard_Wildcard( lowerBound = asSuperWildcardLowerBound(type.getSuperBound(), superType, p); } superType.setSuperBound(lowerBound); - isUninferredTypeAgrument = oldIsUninferredTypeArgument; + isUninferredTypeArgument = oldIsUninferredTypeArgument; annotatedTypeFactory.addDefaultAnnotations(superType); return copyPrimaryAnnos(type, superType); } - public boolean sameAnnotatedTypeFactory(AnnotatedTypeFactory annotatedTypeFactory) { + /** + * Returns true if the annotatedTypeFactory for this is the given value. + * + * @param annotatedTypeFactory a factory to compare to that of this + * @return true if the annotatedTypeFactory for this is the given value + */ + public boolean sameAnnotatedTypeFactory( + @FindDistinct AnnotatedTypeFactory annotatedTypeFactory) { return this.annotatedTypeFactory == annotatedTypeFactory; } // diff --git a/framework/src/main/java/org/checkerframework/framework/type/BoundsInitializer.java b/framework/src/main/java/org/checkerframework/framework/type/BoundsInitializer.java index d9b878242a9..3a82b83893d 100644 --- a/framework/src/main/java/org/checkerframework/framework/type/BoundsInitializer.java +++ b/framework/src/main/java/org/checkerframework/framework/type/BoundsInitializer.java @@ -19,6 +19,7 @@ import javax.lang.model.type.TypeMirror; import javax.lang.model.type.TypeVariable; import javax.lang.model.type.WildcardType; +import org.checkerframework.checker.interning.qual.FindDistinct; import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedArrayType; import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedDeclaredType; import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedExecutableType; @@ -802,7 +803,7 @@ public TypePathNode addPathNode(TypePathNode node) { * * @param node last node in the path */ - public void removePathNode(TypePathNode node) { + public void removePathNode(@FindDistinct TypePathNode node) { if (currentPath.getLeaf() != node) { throw new BugInCF( "Cannot remove node: %s. It is not the last node. currentPath= %s", diff --git a/framework/src/main/java/org/checkerframework/framework/type/DefaultTypeHierarchy.java b/framework/src/main/java/org/checkerframework/framework/type/DefaultTypeHierarchy.java index 8c4c9b996f2..333d0363022 100644 --- a/framework/src/main/java/org/checkerframework/framework/type/DefaultTypeHierarchy.java +++ b/framework/src/main/java/org/checkerframework/framework/type/DefaultTypeHierarchy.java @@ -8,6 +8,7 @@ import javax.lang.model.type.TypeMirror; import javax.lang.model.type.TypeVariable; import javax.lang.model.util.Types; +import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.common.basetype.BaseTypeChecker; import org.checkerframework.framework.qual.Covariant; import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedArrayType; @@ -31,12 +32,13 @@ * covariant array types, raw types, and allowing covariant type arguments depending on various * options passed to DefaultTypeHierarchy. * - *

        Subtyping rules of the JLS can be found in section 4.10, "Subtyping": - * https://docs.oracle.com/javase/specs/jls/se11/html/jls-4.html#jls-4.10 + *

        Subtyping rules of the JLS can be found in section 4.10, + * "Subtyping". * *

        Note: The visit methods of this class must be public but it is intended to be used through a - * TypeHierarchy interface reference which will only allow isSubtype to be called. It does not make - * sense to call the visit methods on their own. + * TypeHierarchy interface reference which will only allow isSubtype to be called. Clients should + * not call the visit methods. */ public class DefaultTypeHierarchy extends AbstractAtmComboVisitor implements TypeHierarchy { @@ -51,13 +53,13 @@ public class DefaultTypeHierarchy extends AbstractAtmComboVisitor // TODO: Incorporate feedback from David/Suzanne // IMPORTANT_NOTE: - + // // For MultigraphQualifierHierarchies, we check the subtyping relationship of each annotation // hierarchy individually. This is done because when comparing a pair of type variables, // sometimes you need to traverse and compare the bounds of two type variables. Other times it // is incorrect to compare the bounds. These two cases can occur simultaneously when comparing - // two hierarchies at once. In this case, comparing both hierarchies simultaneously will leadd - // ot an error. More detail is given below. + // two hierarchies at once. In this case, comparing both hierarchies simultaneously will lead + // to an error. More detail is given below. // // Recall, type variables may or may not have a primary annotation for each individual // hierarchy. When comparing @@ -141,15 +143,16 @@ protected StructuralEqualityComparer createEqualityComparer() { } /** - * Returns true if subtype {@literal <:} supertype. This implementation iterates over all top - * annotations and invokes {@link #isSubtype(AnnotatedTypeMirror, AnnotatedTypeMirror, - * AnnotationMirror)}. Most type systems should not override this method, but instead override - * {@link #isSubtype(AnnotatedTypeMirror, AnnotatedTypeMirror, AnnotationMirror)} or some of the - * {@code visitXXX} methods. + * Returns true if subtype {@literal <:} supertype. + * + *

        This implementation iterates over all top annotations and invokes {@link + * #isSubtype(AnnotatedTypeMirror, AnnotatedTypeMirror, AnnotationMirror)}. Most type systems + * should not override this method, but instead override {@link #isSubtype(AnnotatedTypeMirror, + * AnnotatedTypeMirror, AnnotationMirror)} or some of the {@code visitXXX} methods. * * @param subtype expected subtype * @param supertype expected supertype - * @return true if subtype is actually a subtype of supertype + * @return true if subtype is a subtype of supertype or equal to it */ @Override public boolean isSubtype( @@ -169,9 +172,9 @@ public boolean isSubtype( * * @param subtype expected subtype * @param supertype expected supertype - * @param top the hierarchy for which we want to make a comparison - * @return returns true if {@code subtype} is a subtype of {@code supertype} in the qualifier - * hierarchy whose top is {@code top} + * @param top the top of the hierarchy for which we want to make a comparison + * @return returns true if {@code subtype} is a subtype of, or equal to, {@code supertype} in + * the qualifier hierarchy whose top is {@code top} */ protected boolean isSubtype( final AnnotatedTypeMirror subtype, @@ -199,8 +202,8 @@ protected String defaultErrorMessage( } /** - * Compare the primary annotations of subtype and supertype. Neither type can be missing - * annotations. + * Compare the primary annotations of {@code subtype} and {@code supertype}. Neither type can be + * missing annotations. * * @return true if the primary annotation on subtype {@literal <:} primary annotation on * supertype for the current top. @@ -210,7 +213,7 @@ protected boolean isPrimarySubtype(AnnotatedTypeMirror subtype, AnnotatedTypeMir } /** - * Compare the primary annotations of subtype and supertype. + * Compare the primary annotations of {@code subtype} and {@code supertype}. * * @param annosCanBeEmtpy indicates that annotations may be missing from the typemirror * @return true if the primary annotation on subtype {@literal <:} primary annotation on @@ -223,7 +226,7 @@ protected boolean isPrimarySubtype( final AnnotationMirror supertypeAnno = supertype.getAnnotationInHierarchy(currentTop); if (checker.getTypeFactory().hasQualifierParameterInHierarchy(supertype, currentTop) && checker.getTypeFactory().hasQualifierParameterInHierarchy(subtype, currentTop)) { - // Qualifiers must be equivalent. + // If the types have a class qualifier parameter, the qualifiers must be equivalent. return isAnnoSubtype(subtypeAnno, supertypeAnno, annosCanBeEmtpy) && isAnnoSubtype(supertypeAnno, subtypeAnno, annosCanBeEmtpy); } @@ -232,16 +235,18 @@ protected boolean isPrimarySubtype( } /** - * Compare the primary annotations of subtype and supertype. + * Compare the primary annotations of {@code subtype} and {@code supertype}. * - * @param subtypeAnno annotation we expect to be a subtype - * @param supertypeAnno annotation we expect to be a supertype of subtype + * @param subtypeAnno annotation that may be a subtype + * @param supertypeAnno annotation that may be a supertype * @param annosCanBeEmtpy indicates that annotations may be missing from the typemirror * @return true if subtype {@literal <:} supertype or both annotations are null. False is * returned if one annotation is null and the other is not. */ protected boolean isAnnoSubtype( - AnnotationMirror subtypeAnno, AnnotationMirror supertypeAnno, boolean annosCanBeEmtpy) { + @Nullable AnnotationMirror subtypeAnno, + @Nullable AnnotationMirror supertypeAnno, + boolean annosCanBeEmtpy) { if (annosCanBeEmtpy && subtypeAnno == null && supertypeAnno == null) { return true; } @@ -250,16 +255,15 @@ protected boolean isAnnoSubtype( } /** - * Checks to see if subtype is bottom (if a bottom exists) If there is no explicit bottom then - * false is returned. + * Returns true if subtype is the bottom type in the current hierarchy. * - * @param subtype type to isValid against bottom + * @param subtype type to test against bottom * @return true if subtype's primary annotation is bottom */ protected boolean isBottom(final AnnotatedTypeMirror subtype) { final AnnotationMirror bottom = qualifierHierarchy.getBottomAnnotation(currentTop); if (bottom == null) { - return false; // can't be below infinitely sized hierarchy + throw new BugInCF("getBottomAnnotation(%s) is null", currentTop); } switch (subtype.getKind()) { @@ -280,17 +284,22 @@ protected boolean isBottom(final AnnotatedTypeMirror subtype) { } /** - * Check and subtype first determines if the subtype/supertype combination has already been - * visited. If so, it returns true, otherwise add the subtype/supertype combination and then - * make a subtype check + * Like {@link #isSubtype(AnnotatedTypeMirror, AnnotatedTypeMirror)}, but uses a cache to + * prevent infinite recursion on recursive types. + * + * @param subtype a type that may be a subtype + * @param supertype a type that may be a supertype + * @return true if subtype {@literal <:} supertype */ - protected boolean checkAndSubtype( + protected boolean isSubtypeCaching( final AnnotatedTypeMirror subtype, final AnnotatedTypeMirror supertype) { if (visitHistory.contains(subtype, supertype, currentTop)) { + // visitHistory only contains pairs in a subtype relationship. return true; } boolean result = isSubtype(subtype, supertype, currentTop); + // The call to add has no effect if result is false. visitHistory.add(subtype, supertype, currentTop, result); return result; } @@ -321,14 +330,15 @@ protected boolean areEqualInHierarchy( /** * A declared type is considered a supertype of another declared type only if all of the type * arguments of the declared type "contain" the corresponding type arguments of the subtype. - * Containment is described in the JLS section 4.5.1 "Type Arguments of Parameterized Types", - * https://docs.oracle.com/javase/specs/jls/se11/html/jls-4.html#jls-4.5.1 + * Containment is described in JLS section + * 4.5.1 "Type Arguments of Parameterized Types". * - * @param inside the "subtype" type argument - * @param outside the "supertype" type argument + * @param inside a type argument of the "subtype" + * @param outside a type argument of the "supertype" * @param canBeCovariant whether or not type arguments are allowed to be covariant - * @return true if inside is contained by outside OR, if canBeCovariant == true, inside is a - * subtype of outside + * @return true if inside is contained by outside, or if canBeCovariant == true and {@code + * inside <: outside} */ protected boolean isContainedBy( final AnnotatedTypeMirror inside, @@ -399,7 +409,7 @@ private boolean isContainedWildcard( AnnotatedTypeMirror castedInside = AnnotatedTypes.castedAsSuper(inside.atypeFactory, inside, outsideUpperBound); - if (!checkAndSubtype(castedInside, outsideUpperBound)) { + if (!isSubtypeCaching(castedInside, outsideUpperBound)) { return false; } @@ -408,7 +418,7 @@ private boolean isContainedWildcard( // tests/all-systems/Issue1991.java crashes without this. return true; } - return canBeCovariant || checkAndSubtype(outsideLowerBound, inside); + return canBeCovariant || isSubtypeCaching(outsideLowerBound, inside); } private boolean ignoreUninferredTypeArgument(AnnotatedTypeMirror type) { @@ -422,6 +432,10 @@ private boolean ignoreUninferredTypeArgument(AnnotatedTypeMirror type) { return false; } + // ------------------------------------------------------------------------ + // The rest of this file is the visitor methods. It is a lot of methods, one for each + // combination of types. + // ------------------------------------------------------------------------ // Arrays as subtypes @@ -1002,10 +1016,16 @@ protected boolean visitIntersectionSupertype( return result; } - /** A type variable is a supertype if its lower bound is above subtype. */ + /** + * A type variable is a supertype if its lower bound is above subtype. + * + * @param subtype a type that might be a subtype + * @param supertype a type that might be a supertype + * @return true if {@code subtype} is a subtype of {@code supertype} + */ protected boolean visitTypevarSupertype( AnnotatedTypeMirror subtype, AnnotatedTypeVariable supertype) { - return checkAndSubtype(subtype, supertype.getLowerBound()); + return isSubtypeCaching(subtype, supertype.getLowerBound()); } /** @@ -1048,9 +1068,8 @@ && isPrimarySubtype(ub, supertype)) { } return false; } - return isPrimarySubtype(upperBound, supertype); } - return checkAndSubtype(upperBound, supertype); + return isSubtypeCaching(upperBound, supertype); } /** A union type is a subtype if ALL of its alternatives are subtypes of supertype. */ diff --git a/framework/src/main/java/org/checkerframework/framework/type/ElementAnnotationApplier.java b/framework/src/main/java/org/checkerframework/framework/type/ElementAnnotationApplier.java index c4a7223f51b..4370a7aa8df 100644 --- a/framework/src/main/java/org/checkerframework/framework/type/ElementAnnotationApplier.java +++ b/framework/src/main/java/org/checkerframework/framework/type/ElementAnnotationApplier.java @@ -32,9 +32,9 @@ * represents that element (or a use of that Element). * *

        In a way, this class is a hack: the Type representation for the Elements should contain all - * annotations that we want. However, due to javac bugs - * http://mail.openjdk.java.net/pipermail/type-annotations-dev/2013-December/001449.html decoding - * the type annotations from the Element is necessary. + * annotations that we want. However, due to javac + * bugs decoding the type annotations from the Element is necessary. * *

        Even once these bugs are fixed, this class might be useful: in TypesIntoElements it is easy to * add additional annotations to the element and have them stored in the bytecode by the compiler. diff --git a/framework/src/main/java/org/checkerframework/framework/type/EqualityAtmComparer.java b/framework/src/main/java/org/checkerframework/framework/type/EqualityAtmComparer.java index 3bc6163d42f..33136be1cb3 100644 --- a/framework/src/main/java/org/checkerframework/framework/type/EqualityAtmComparer.java +++ b/framework/src/main/java/org/checkerframework/framework/type/EqualityAtmComparer.java @@ -1,5 +1,6 @@ package org.checkerframework.framework.type; +import org.checkerframework.checker.interning.qual.EqualsMethod; import org.checkerframework.framework.type.visitor.EquivalentAtmComboScanner; import org.checkerframework.javacutil.AnnotationUtils; import org.checkerframework.javacutil.SystemUtil; @@ -40,24 +41,33 @@ protected boolean arePrimeAnnosEqual( return AnnotationUtils.areSame(type1.getAnnotations(), type2.getAnnotations()); } + /** + * Return true if the twe types are the same. + * + * @param type1 the first type to compare + * @param type2 the second type to compare + * @return true if the twe types are the same + */ + @EqualsMethod // to make Interning Checker permit the == comparison protected boolean compare(final AnnotatedTypeMirror type1, AnnotatedTypeMirror type2) { - if ((type1 == null && type2 != null) || (type1 != null && type2 == null)) { - return false; - } - if (type1 == type2) { return true; } + if (type1 == null || type2 == null) { + return false; + } + @SuppressWarnings("TypeEquals") // TODO boolean sameUnderlyingType = type1.getUnderlyingType().equals(type2.getUnderlyingType()); return sameUnderlyingType && arePrimeAnnosEqual(type1, type2); } + @SuppressWarnings("interning:not.interned") @Override protected Boolean scanWithNull( AnnotatedTypeMirror type1, AnnotatedTypeMirror type2, Void aVoid) { - // one of them should be null, therefore they are only equal if they other is null + // one of them should be null, therefore they are only equal if the other is null return type1 == type2; } diff --git a/framework/src/main/java/org/checkerframework/framework/type/GenericAnnotatedTypeFactory.java b/framework/src/main/java/org/checkerframework/framework/type/GenericAnnotatedTypeFactory.java index 284e8ca7e65..cca17d53474 100644 --- a/framework/src/main/java/org/checkerframework/framework/type/GenericAnnotatedTypeFactory.java +++ b/framework/src/main/java/org/checkerframework/framework/type/GenericAnnotatedTypeFactory.java @@ -124,9 +124,12 @@ public abstract class GenericAnnotatedTypeFactory< /** to annotate types based on the given tree */ protected TypeAnnotator typeAnnotator; - /** for use in addAnnotationsFromDefaultQualifierForUse */ + /** for use in addAnnotationsFromDefaultForType */ private DefaultQualifierForUseTypeAnnotator defaultQualifierForUseTypeAnnotator; + /** for use in addAnnotationsFromDefaultForType */ + private DefaultForTypeAnnotator defaultForTypeAnnotator; + /** to annotate types based on the given un-annotated types */ protected TreeAnnotator treeAnnotator; @@ -162,6 +165,33 @@ public abstract class GenericAnnotatedTypeFactory< */ private boolean shouldDefaultTypeVarLocals; + /** + * Elements representing variables for which the type of the initializer is being determined in + * order to apply qualifier parameter defaults. + * + *

        Local variables with a qualifier parameter get their declared type from the type of their + * initializer. Sometimes the initializer's type depends on the type of the variable, such as + * during type variable inference or when a variable is used in its own initializer as in + * "Object o = (o = null)". This creates a circular dependency resulting in infinite recursion. + * To prevent this, variables in this set should not be typed based on their initializer, but by + * using normal defaults. + * + *

        This set should only be modified in + * GenericAnnotatedTypeFactory#applyLocalVariableQualifierParameterDefaults which clears + * variables after computing their initializer types. + * + * @see GenericAnnotatedTypeFactory#applyLocalVariableQualifierParameterDefaults + */ + private Set variablesUnderInitialization; + + /** + * Caches types of initializers for local variables with a qualifier parameter, so that they + * aren't computed each time the type of a variable is looked up. + * + * @see GenericAnnotatedTypeFactory#applyLocalVariableQualifierParameterDefaults + */ + private Map initializerCache; + /** An empty store. */ // Set in postInit only protected Store emptyStore; @@ -204,6 +234,7 @@ protected GenericAnnotatedTypeFactory(BaseTypeChecker checker, boolean useFlow) this.shouldDefaultTypeVarLocals = useFlow; this.useFlow = useFlow; + this.variablesUnderInitialization = new HashSet<>(); this.scannedClasses = new HashMap<>(); this.flowResult = null; this.regularExitStores = null; @@ -219,8 +250,10 @@ protected GenericAnnotatedTypeFactory(BaseTypeChecker checker, boolean useFlow) if (shouldCache) { int cacheSize = getCacheSize(); flowResultAnalysisCaches = CollectionUtils.createLRUCache(cacheSize); + initializerCache = CollectionUtils.createLRUCache(cacheSize); } else { flowResultAnalysisCaches = null; + initializerCache = null; } // Every subclass must call postInit, but it must be called after @@ -236,6 +269,7 @@ protected void postInit() { this.treeAnnotator = createTreeAnnotator(); this.typeAnnotator = createTypeAnnotator(); this.defaultQualifierForUseTypeAnnotator = createDefaultForUseTypeAnnotator(); + this.defaultForTypeAnnotator = createDefaultForTypeAnnotator(); this.poly = createQualifierPolymorphism(); @@ -283,6 +317,7 @@ public void setRoot(@Nullable CompilationUnitTree root) { if (shouldCache) { this.flowResultAnalysisCaches.clear(); + this.initializerCache.clear(); this.defaultQualifierForUseTypeAnnotator.clearCache(); } } @@ -352,8 +387,6 @@ protected TreeAnnotator createTreeAnnotator() { *

      • {@link IrrelevantTypeAnnotator}: Adds top to types not listed in the {@link * RelevantJavaTypes} annotation on the checker. *
      • {@link PropagationTypeAnnotator}: Propagates annotation onto wildcards. - *
      • {@link DefaultForTypeAnnotator}: Adds annotations based on {@link DefaultFor} - * meta-annotations. * * * @return a type annotator @@ -370,15 +403,27 @@ protected TypeAnnotator createTypeAnnotator() { this, getQualifierHierarchy().getTopAnnotations(), relevantClasses)); } typeAnnotators.add(new PropagationTypeAnnotator(this)); - typeAnnotators.add(new DefaultForTypeAnnotator(this)); return new ListTypeAnnotator(typeAnnotators); } - /** Creates an {@link DefaultQualifierForUseTypeAnnotator}. */ + /** + * Creates an {@link DefaultQualifierForUseTypeAnnotator}. + * + * @return a new {@link DefaultQualifierForUseTypeAnnotator} + */ protected DefaultQualifierForUseTypeAnnotator createDefaultForUseTypeAnnotator() { return new DefaultQualifierForUseTypeAnnotator(this); } + /** + * Creates an {@link DefaultForTypeAnnotator}. + * + * @return a new {@link DefaultForTypeAnnotator} + */ + protected DefaultForTypeAnnotator createDefaultForTypeAnnotator() { + return new DefaultForTypeAnnotator(this); + } + /** * Returns the appropriate flow analysis class that is used for the * org.checkerframework.dataflow analysis. @@ -1172,10 +1217,14 @@ protected void performFlowAnalysis(ClassTree classTree) { while (!lambdaQueue.isEmpty()) { Pair lambdaPair = lambdaQueue.poll(); + MethodTree mt = + (MethodTree) + TreeUtils.enclosingOfKind( + getPath(lambdaPair.first), Kind.METHOD); analyze( queue, lambdaQueue, - new CFGLambda(lambdaPair.first), + new CFGLambda(lambdaPair.first, classTree, mt), fieldValues, classTree, false, @@ -1487,7 +1536,7 @@ public AnnotatedTypeMirror getMethodReturnType(MethodTree m, ReturnTree r) { @Override public void addDefaultAnnotations(AnnotatedTypeMirror type) { - addAnnotationsFromDefaultQualifierForUse(null, type); + addAnnotationsFromDefaultForType(null, type); typeAnnotator.visit(type, null); defaults.annotate((Element) null, type); } @@ -1506,6 +1555,10 @@ protected final void addComputedTypeAnnotations(Tree tree, AnnotatedTypeMirror t /** * Like {@link #addComputedTypeAnnotations(Tree, AnnotatedTypeMirror)}. Overriding * implementations typically simply pass the boolean to calls to super. + * + * @param tree an AST node + * @param type the type obtained from tree + * @param iUseFlow whether to use information from dataflow analysis */ protected void addComputedTypeAnnotations( Tree tree, AnnotatedTypeMirror type, boolean iUseFlow) { @@ -1513,9 +1566,18 @@ protected void addComputedTypeAnnotations( : "GenericAnnotatedTypeFactory.addComputedTypeAnnotations: " + " root needs to be set when used on trees; factory: " + this.getClass(); - addAnnotationsFromDefaultQualifierForUse(TreeUtils.elementFromTree(tree), type); + + if (!TreeUtils.isExpressionTree(tree)) { + // Don't apply defaults to expressions. Their types may be computed from subexpressions + // in treeAnnotator. + addAnnotationsFromDefaultForType(TreeUtils.elementFromTree(tree), type); + } applyQualifierParameterDefaults(tree, type); treeAnnotator.visit(tree, type); + if (TreeUtils.isExpressionTree(tree)) { + // If a tree annotator, did not add a type, add the DefaultForUse default. + addAnnotationsFromDefaultForType(TreeUtils.elementFromTree(tree), type); + } typeAnnotator.visit(type, null); defaults.annotate(tree, type); @@ -1588,6 +1650,11 @@ protected void applyInferredAnnotations(AnnotatedTypeMirror type, Value as) { /** * Applies defaults for types in a class with an qualifier parameter. * + *

        Within a class with {@code @HasQualifierParameter}, types with that class default to the + * polymorphic qualifier rather than the typical default. Local variables with a type that has a + * qualifier parameter are initialized to the type of their initializer, rather than the default + * for local variables. + * * @param tree Tree whose type is {@code type} * @param type where the defaults are applied */ @@ -1598,6 +1665,11 @@ protected void applyQualifierParameterDefaults(Tree tree, AnnotatedTypeMirror ty /** * Applies defaults for types in a class with an qualifier parameter. * + *

        Within a class with {@code @HasQualifierParameter}, types with that class default to the + * polymorphic qualifier rather than the typical default. Local variables with a type that has a + * qualifier parameter are initialized to the type of their initializer, rather than the default + * for local variables. + * * @param elt Element whose type is {@code type} * @param type where the defaults are applied */ @@ -1617,6 +1689,8 @@ protected void applyQualifierParameterDefaults( return; } + applyLocalVariableQualifierParameterDefaults(elt, type); + TypeElement enclosingClass = ElementUtils.enclosingClass(elt); Set tops; if (enclosingClass != null) { @@ -1645,6 +1719,64 @@ public Void visitDeclared(AnnotatedDeclaredType type, Void aVoid) { }.visit(type); } + /** + * Defaults local variables with types that have a qualifier parameter to the type of their + * initializer, if an initializer is present. Does nothing for local variables with no + * initializer. + * + * @param elt Element whose type is {@code type} + * @param type where the defaults are applied + */ + private void applyLocalVariableQualifierParameterDefaults( + Element elt, AnnotatedTypeMirror type) { + if (elt.getKind() != ElementKind.LOCAL_VARIABLE + || getQualifierParameterHierarchies(type).isEmpty() + || variablesUnderInitialization.contains(elt)) { + return; + } + + Tree declTree = declarationFromElement(elt); + if (declTree == null || declTree.getKind() != Kind.VARIABLE) { + return; + } + + ExpressionTree initializer = ((VariableTree) declTree).getInitializer(); + if (initializer == null) { + return; + } + + VariableElement variableElt = (VariableElement) elt; + variablesUnderInitialization.add(variableElt); + AnnotatedTypeMirror initializerType; + if (shouldCache && initializerCache.containsKey(initializer)) { + initializerType = initializerCache.get(initializer); + } else { + // When this method is called by getAnnotatedTypeLhs, flow is turned off. + // Turn it back on so the type of the initializer is the refined type. + boolean oldUseFlow = useFlow; + useFlow = everUseFlow; + try { + initializerType = getAnnotatedType(initializer); + } finally { + useFlow = oldUseFlow; + } + } + + Set qualParamTypes = AnnotationUtils.createAnnotationSet(); + for (AnnotationMirror initializerAnnotation : initializerType.getAnnotations()) { + if (hasQualifierParameterInHierarchy( + type, qualHierarchy.getTopAnnotation(initializerAnnotation))) { + qualParamTypes.add(initializerAnnotation); + } + } + + type.addMissingAnnotations(qualParamTypes); + variablesUnderInitialization.remove(variableElt); + if (shouldCache) { + initializerCache.put(initializer, initializerType); + } + } + /** * To add annotations to the type of method or constructor parameters, add a {@link * TypeAnnotator} using {@link #createTypeAnnotator()} and see the comment in {@link @@ -1656,7 +1788,7 @@ public Void visitDeclared(AnnotatedDeclaredType type, Void aVoid) { */ @Override public void addComputedTypeAnnotations(Element elt, AnnotatedTypeMirror type) { - addAnnotationsFromDefaultQualifierForUse(elt, type); + addAnnotationsFromDefaultForType(elt, type); applyQualifierParameterDefaults(elt, type); typeAnnotator.visit(type, null); defaults.annotate(elt, type); @@ -1738,6 +1870,11 @@ public boolean getShouldDefaultTypeVarLocals() { /** The CFGVisualizer to be used by all CFAbstractAnalysis instances. */ protected final CFGVisualizer cfgVisualizer; + /** + * Create a new CFGVisualizer. + * + * @return a new CFGVisualizer + */ protected CFGVisualizer createCFGVisualizer() { if (checker.hasOption("flowdotdir")) { String flowdotdir = checker.getOption("flowdotdir"); @@ -1793,8 +1930,11 @@ private String getCheckerName() { return checkerName; } - /* Parse values or key-value pairs into a map from value to true, respectively, - * from the value to the key. + /** + * Parse keys or key-value pairs into a map from key to value (to true if no value is provided). + * + * @param opts the CFG visualization options + * @return a map that represents the options */ private Map processCFGVisualizerOption(String[] opts) { Map res = new HashMap<>(opts.length - 1); @@ -1810,7 +1950,7 @@ private Map processCFGVisualizerOption(String[] opts) { res.put(split[0], split[1]); break; default: - throw new UserError("Too many `=` in cfgviz option: " + opt); + throw new UserError("Too many '=' in cfgviz option: " + opt); } } return res; @@ -1834,25 +1974,36 @@ public void postAsMemberOf( * Adds default qualifiers bases on the underlying type of {@code type} to {@code type}. If * {@code element} is a local variable, then the defaults are not added. * + *

        (This uses both the {@link DefaultQualifierForUseTypeAnnotator} and {@link + * DefaultForTypeAnnotator}.) + * * @param element possibly null element whose type is {@code type} * @param type the type to which defaults are added */ - protected void addAnnotationsFromDefaultQualifierForUse( + protected void addAnnotationsFromDefaultForType( @Nullable Element element, AnnotatedTypeMirror type) { - if (element != null - && element.getKind() == ElementKind.LOCAL_VARIABLE - && type.getKind() == TypeKind.DECLARED) { - // If this is a type for a local variable, don't apply the default to the primary - // location. - AnnotatedDeclaredType declaredType = (AnnotatedDeclaredType) type; - if (declaredType.getEnclosingType() != null) { - defaultQualifierForUseTypeAnnotator.visit(declaredType.getEnclosingType()); - } - for (AnnotatedTypeMirror typeArg : declaredType.getTypeArguments()) { - defaultQualifierForUseTypeAnnotator.visit(typeArg); + if (element != null && element.getKind() == ElementKind.LOCAL_VARIABLE) { + if (type.getKind() == TypeKind.DECLARED) { + // If this is a type for a local variable, don't apply the default to the primary + // location. + AnnotatedDeclaredType declaredType = (AnnotatedDeclaredType) type; + if (declaredType.getEnclosingType() != null) { + defaultQualifierForUseTypeAnnotator.visit(declaredType.getEnclosingType()); + defaultForTypeAnnotator.visit(declaredType.getEnclosingType()); + } + for (AnnotatedTypeMirror typeArg : declaredType.getTypeArguments()) { + defaultQualifierForUseTypeAnnotator.visit(typeArg); + defaultForTypeAnnotator.visit(typeArg); + } + } else if (type.getKind().isPrimitive()) { + // Don't apply the default for local variables with primitive types. + } else { + defaultQualifierForUseTypeAnnotator.visit(type); + defaultForTypeAnnotator.visit(type); } } else { defaultQualifierForUseTypeAnnotator.visit(type); + defaultForTypeAnnotator.visit(type); } } } diff --git a/framework/src/main/java/org/checkerframework/framework/type/QualifierHierarchy.java b/framework/src/main/java/org/checkerframework/framework/type/QualifierHierarchy.java index 4d5a52ac84d..b75b0f87e49 100644 --- a/framework/src/main/java/org/checkerframework/framework/type/QualifierHierarchy.java +++ b/framework/src/main/java/org/checkerframework/framework/type/QualifierHierarchy.java @@ -5,25 +5,34 @@ import java.util.Set; import javax.lang.model.element.AnnotationMirror; import javax.lang.model.type.TypeKind; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.framework.qual.AnnotatedFor; import org.checkerframework.javacutil.AnnotationUtils; import org.checkerframework.javacutil.BugInCF; /** - * Represents a type qualifier hierarchy. + * Represents multiple type qualifier hierarchies. {@link #getWidth} gives the number of hierarchies + * that this object represents. Each hierarchy has its own top and bottom, and subtyping + * relationships exist only within each hierarchy. * - *

        All method parameter annotations need to be type qualifiers recognized within this hierarchy. + *

        Note the distinction in terminology between a qualifier hierarchy, which has one top and one + * bottom, and a {@code QualifierHierarchy}, which represents multiple qualifier hierarchies. * - *

        This assumes that any particular annotated type in a program is annotated with at least one - * qualifier from the hierarchy. + *

        All type annotations need to be type qualifiers recognized within this hierarchy. + * + *

        This assumes that every annotated type in a program is annotated with exactly one qualifier + * from each hierarchy. */ -public abstract class QualifierHierarchy { +@AnnotatedFor("nullness") +public interface QualifierHierarchy { /** - * Determine whether the instance is valid. + * Determine whether this is valid. * - * @return whether the instance is valid + * @return whether this is valid */ - public abstract boolean isValid(); + boolean isValid(); // ********************************************************************** // Getter methods about this hierarchy @@ -32,70 +41,99 @@ public abstract class QualifierHierarchy { /** * Returns the width of this hierarchy, i.e. the expected number of annotations on any valid * type. + * + * @return the width of this QualifierHierarchy */ - public int getWidth() { + default int getWidth() { return getTopAnnotations().size(); } /** - * Returns the top (ultimate super) type qualifiers in the type system. + * Returns the top (ultimate super) type qualifiers in the type system. The size of this set is + * equal to {@link #getWidth}. * * @return the top (ultimate super) type qualifiers in the type system */ - public abstract Set getTopAnnotations(); + Set getTopAnnotations(); /** * Return the top qualifier for the given qualifier, that is, the qualifier that is a supertype - * of start but no further supertypes exist. + * of {@code qualifier} but no further supertypes exist. + * + * @param qualifier any qualifier from one of the qualifier hierarchies represented by this + * @return the top qualifier of {@code qualifier}'s hierarchy */ - public abstract AnnotationMirror getTopAnnotation(AnnotationMirror start); + AnnotationMirror getTopAnnotation(AnnotationMirror qualifier); /** - * Return the bottom for the given qualifier, that is, the qualifier that is a subtype of start - * but no further subtypes exist. + * Returns the bottom type qualifiers in the hierarchy. The size of this set is equal to {@link + * #getWidth}. + * + * @return the bottom type qualifiers in the hierarchy */ - public abstract AnnotationMirror getBottomAnnotation(AnnotationMirror start); + Set getBottomAnnotations(); /** - * Returns the bottom type qualifier in the hierarchy. + * Return the bottom for the given qualifier, that is, the qualifier that is a subtype of {@code + * qualifier} but no further subtypes exist. * - * @return the bottom type qualifier in the hierarchy + * @param qualifier any qualifier from one of the qualifier hierarchies represented by this + * @return the bottom qualifier of {@code qualifier}'s hierarchy */ - public abstract Set getBottomAnnotations(); + AnnotationMirror getBottomAnnotation(AnnotationMirror qualifier); /** - * Returns the polymorphic qualifier for that hierarchy or {@code null} if there is no - * polymorphic qualifier in that hierarchy. + * Returns the polymorphic qualifier for the hierarchy containing {@code qualifier}, or {@code + * null} if there is no polymorphic qualifier in that hierarchy. * - * @param start any qualifier from the type hierarchy - * @return the polymorphic qualifier for that hierarchy or {@code null} if there is no - * polymorphic qualifier in that hierarchy + * @param qualifier any qualifier from one of the qualifier hierarchies represented by this + * @return the polymorphic qualifier for the hierarchy containing {@code qualifier}, or {@code + * null} if there is no polymorphic qualifier in that hierarchy */ - public abstract AnnotationMirror getPolymorphicAnnotation(AnnotationMirror start); + @Nullable AnnotationMirror getPolymorphicAnnotation(AnnotationMirror qualifier); + + /** + * Returns {@code true} if the qualifier is a polymorphic qualifier; otherwise, returns {@code + * false}. + * + * @param qualifier qualifier + * @return {@code true} if the qualifier is a polymorphic qualifier; otherwise, returns {@code + * false}. + */ + boolean isPolymorphicQualifier(AnnotationMirror qualifier); // ********************************************************************** // Qualifier Hierarchy Queries // ********************************************************************** /** - * Tests whether rhs is equal to or a sub-qualifier of lhs, according to the type qualifier - * hierarchy. This checks only the qualifiers, not the Java type. + * Tests whether {@code subQualifier} is equal to or a sub-qualifier of {@code superQualifier}, + * according to the type qualifier hierarchy. * - * @return true iff rhs is a sub qualifier of lhs + * @param subQualifier possible subqualifier of {@code superQualifier} + * @param superQualifier possible superqualifier of {@code subQualifier} + * @return true iff {@code subQualifier} is a subqualifier of, or equal to, {@code + * superQualifier} */ - public abstract boolean isSubtype(AnnotationMirror rhs, AnnotationMirror lhs); + boolean isSubtype(AnnotationMirror subQualifier, AnnotationMirror superQualifier); /** - * Tests whether there is any annotation in lhs that is a super qualifier of, or equal to, some - * annotation in rhs. lhs and rhs contain only the annotations, not the Java type. + * Tests whether all qualifiers in {@code subQualifiers} are a subqualifier or equal to the + * qualifier in the same hierarchy in {@code superQualifiers}. * - * @return true iff an annotation in lhs is a super of one in rhs + * @param subQualifiers set of qualifiers; exactly one per hierarchy + * @param superQualifiers set of qualifiers; exactly one per hierarchy + * @return true iff all qualifiers in {@code subQualifiers} are a subqualifier or equal to the + * qualifier in the same hierarchy in {@code superQualifiers} */ - public abstract boolean isSubtype( - Collection rhs, Collection lhs); + boolean isSubtype( + Collection subQualifiers, + Collection superQualifiers); /** - * Returns the least upper bound for the qualifiers a1 and a2. + * Returns the least upper bound (LUB) of the qualifiers {@code qualifier1} and {@code + * qualifier2}. Returns {@code null} if the qualifiers are not from the same qualifier + * hierarchy. * *

        Examples: * @@ -103,126 +141,124 @@ public abstract boolean isSubtype( *

      • For NonNull, leastUpperBound('Nullable', 'NonNull') ⇒ Nullable * * - * The two qualifiers have to be from the same qualifier hierarchy. Otherwise, null will be - * returned. + * @param qualifier1 the first qualifier; may not be in the same hierarchy as {@code qualifier2} + * @param qualifier2 the second qualifier; may not be in the same hierarchy as {@code + * qualifier1} + * @return the least upper bound of the qualifiers, or {@code null} if the qualifiers are from + * different hierarchies + */ + // The fact that null is returned if the qualifiers are not in the same hierarchy is used by the + // collection version of LUB below. + @Nullable AnnotationMirror leastUpperBound(AnnotationMirror qualifier1, AnnotationMirror qualifier2); + + /** + * Returns the least upper bound of the two sets of qualifiers. The result is the lub of the + * qualifier for the same hierarchy in each set. * - * @return the least restrictive qualifiers for both types + * @param qualifiers1 set of qualifiers; exactly one per hierarchy + * @param qualifiers2 set of qualifiers; exactly one per hierarchy + * @return the least upper bound of the two sets of qualifiers */ - public abstract AnnotationMirror leastUpperBound(AnnotationMirror a1, AnnotationMirror a2); + default Set leastUpperBounds( + Collection qualifiers1, + Collection qualifiers2) { + assertSameSize(qualifiers1, qualifiers2); + if (qualifiers1.isEmpty()) { + throw new BugInCF( + "QualifierHierarchy.leastUpperBounds: tried to determine LUB with empty sets"); + } + + Set result = AnnotationUtils.createAnnotationSet(); + for (AnnotationMirror a1 : qualifiers1) { + for (AnnotationMirror a2 : qualifiers2) { + AnnotationMirror lub = leastUpperBound(a1, a2); + if (lub != null) { + result.add(lub); + } + } + } + + assertSameSize(result, qualifiers1); + return result; + } /** * Returns the number of iterations dataflow should perform before {@link * #widenedUpperBound(AnnotationMirror, AnnotationMirror)} is called or -1 if it should never be * called. * - *

        Subclasses overriding this method should return some positive number or -1. - * * @return the number of iterations dataflow should perform before {@link * #widenedUpperBound(AnnotationMirror, AnnotationMirror)} is called or -1 if it should * never be called. */ - public int numberOfIterationsBeforeWidening() { + default int numberOfIterationsBeforeWidening() { return -1; } /** - * If the type hierarchy has an infinite ascending chain, then the dataflow analysis might never - * reach a fixed point. To prevent this, implement this method such that it returns an upper - * bound for the two qualifiers that is a super type and not equal to the least upper bound. If - * this method is implemented, also override {@link #numberOfIterationsBeforeWidening()} and - * change its return to a positive number. + * If the qualifier hierarchy has an infinite ascending chain, then the dataflow analysis might + * never reach a fixed point. To prevent this, implement this method such that it returns an + * upper bound for the two qualifiers that is a strict super type of the least upper bound. If + * this method is implemented, also override {@link #numberOfIterationsBeforeWidening()} to + * return a positive number. * *

        {@code newQualifier} is newest qualifier dataflow computed for some expression and {@code * previousQualifier} is the qualifier dataflow computed on the last iteration. * - *

        If the type hierarchy has no infinite ascending chain, returns the least upper bound of - * the two annotations. + *

        If the qualifier hierarchy has no infinite ascending chain, returns the least upper bound + * of the two annotations. * - * @param newQualifier new qualifier dataflow computed for some expression - * @param previousQualifier the previous qualifier dataflow computed on the last iteration - * @return an upper bound that is wider than the least upper bound of newQualifier and - * previousQualifier (or the lub if the type hierarchy does not require this) + * @param newQualifier new qualifier dataflow computed for some expression; must be in the same + * hierarchy as {@code previousQualifier} + * @param previousQualifier the previous qualifier dataflow computed on the last iteration; must + * be in the same hierarchy as {@code previousQualifier} + * @return an upper bound that is higher than the least upper bound of newQualifier and + * previousQualifier (or the lub if the qualifier hierarchy does not require this) */ - public AnnotationMirror widenedUpperBound( + default AnnotationMirror widenedUpperBound( AnnotationMirror newQualifier, AnnotationMirror previousQualifier) { - return leastUpperBound(newQualifier, previousQualifier); + AnnotationMirror widenUpperBound = leastUpperBound(newQualifier, previousQualifier); + if (widenUpperBound == null) { + throw new BugInCF( + "Passed two unrelated qualifiers to QualifierHierarchy#widenedUpperBound. %s %s.", + newQualifier, previousQualifier); + } + return widenUpperBound; } /** - * Returns the greatest lower bound for the qualifiers a1 and a2. - * - *

        The two qualifiers have to be from the same qualifier hierarchy. Otherwise, null will be - * returned. + * Returns the greatest lower bound for the qualifiers qualifier1 and qualifier2. Returns null + * if the qualifiers are not from the same qualifier hierarchy. * - * @param a1 first annotation - * @param a2 second annotation - * @return greatest lower bound of the two annotations + * @param qualifier1 first qualifier + * @param qualifier2 second qualifier + * @return greatest lower bound of the two annotations or null if the two annotations are not + * from the same hierarchy */ - public abstract AnnotationMirror greatestLowerBound(AnnotationMirror a1, AnnotationMirror a2); + // The fact that null is returned if the qualifiers are not in the same hierarchy is used by the + // collection version of LUB below. + @Nullable AnnotationMirror greatestLowerBound(AnnotationMirror qualifier1, AnnotationMirror qualifier2); /** - * Returns the least upper bound of two types. Each type is represented as a set of type - * qualifiers, as is the result. - * - *

        Annos1 and annos2 must have the same size, and each annotation in them must be from a - * different type hierarchy. - * - *

        This is necessary for determining the type of a conditional expression ({@code ?:}), where - * the type of the expression is the least upper bound of the true and false clauses. + * Returns the least upper bound of the two sets of qualifiers. The result is the lub of the + * qualifier for the same hierarchy in each set. * - * @param annos1 first collection of qualifiers - * @param annos2 second collection of qualifiers - * @return pairwise least upper bounds of elements of the input collections (which need not be - * sorted in the same order) + * @param qualifiers1 set of qualifiers; exactly one per hierarchy + * @param qualifiers2 set of qualifiers; exactly one per hierarchy + * @return the least upper bound of the two sets of qualifiers */ - public Set leastUpperBounds( - Collection annos1, - Collection annos2) { - assertSameSize(annos1, annos2); - if (annos1.isEmpty()) { - throw new BugInCF( - "QualifierHierarchy.leastUpperBounds: tried to determine LUB with empty sets"); - } - - Set result = AnnotationUtils.createAnnotationSet(); - for (AnnotationMirror a1 : annos1) { - for (AnnotationMirror a2 : annos2) { - AnnotationMirror lub = leastUpperBound(a1, a2); - if (lub != null) { - result.add(lub); - } - } - } - - assertSameSize(result, annos1); - - return result; - } - - /** - * Returns the greatest lower bound of two types. Each type is represented as a set of type - * qualifiers, as is the result. - * - *

        Annos1 and annos2 must have the same size, and each annotation in them must be from a - * different type hierarchy. - * - * @param annos1 first collection of qualifiers - * @param annos2 second collection of qualifiers - * @return pairwise greatest lower bounds of elements of the input collections (which need not - * be sorted in the same order) - */ - public Set greatestLowerBounds( - Collection annos1, - Collection annos2) { - assertSameSize(annos1, annos2); - if (annos1.isEmpty()) { + default Set greatestLowerBounds( + Collection qualifiers1, + Collection qualifiers2) { + assertSameSize(qualifiers1, qualifiers2); + if (qualifiers1.isEmpty()) { throw new BugInCF( "QualifierHierarchy.greatestLowerBounds: tried to determine GLB with empty sets"); } Set result = AnnotationUtils.createAnnotationSet(); - for (AnnotationMirror a1 : annos1) { - for (AnnotationMirror a2 : annos2) { + for (AnnotationMirror a1 : qualifiers1) { + for (AnnotationMirror a2 : qualifiers2) { AnnotationMirror glb = greatestLowerBound(a1, a2); if (glb != null) { result.add(glb); @@ -230,176 +266,158 @@ public Set greatestLowerBounds( } } - assertSameSize(annos1, annos2, result); - + assertSameSize(qualifiers1, qualifiers2, result); return result; } /** - * Tests whether {@code subAnno} is a sub-qualifier of, or equal to, {@code superAnno}, - * according to the type qualifier hierarchy. This checks only the qualifiers, not the Java - * type. + * Returns true if and only if {@link AnnotatedTypeMirror#getAnnotations()} can return a set + * with fewer qualifiers than the width of the QualifierHierarchy. * - *

        This method works even if the underlying Java type is a type variable. In that case, a - * 'null' AnnnotationMirror and the empty set represent a meaningful value (namely, no - * annotation). - * - * @param subAnno a qualifier that might be a subtype - * @param superAnno a qualifier that might be a subtype - * @return true iff {@code subAnno} is a sub qualifier of, or equal to, {@code superAnno} + * @param type the type to test + * @return true if and only if {@link AnnotatedTypeMirror#getAnnotations()} can return a set + * with fewer qualifiers than the width of the QualifierHierarchy */ - public abstract boolean isSubtypeTypeVariable( - AnnotationMirror subAnno, AnnotationMirror superAnno); + static boolean canHaveEmptyAnnotationSet(AnnotatedTypeMirror type) { + return type.getKind() == TypeKind.TYPEVAR + || type.getKind() == TypeKind.WILDCARD + || + // TODO: or should the union/intersection be the LUB of the alternatives? + type.getKind() == TypeKind.UNION + || type.getKind() == TypeKind.INTERSECTION; + } /** - * Tests whether there is any annotation in superAnnos that is a super qualifier of or equal to - * some annotation in subAnnos. superAnnos and subAnnos contain only the annotations, not the - * Java type. - * - *

        This method works even if the underlying Java type is a type variable. In that case, a - * 'null' AnnnotationMirror and the empty set represent a meaningful value (namely, no - * annotation). + * Returns the annotation in qualifiers that is in the same hierarchy as qualifier. * - * @return true iff an annotation in superAnnos is a supertype of, or equal to, one in subAnnos + * @param qualifiers set of annotations to search + * @param qualifier annotation that is in the same hierarchy as the returned annotation + * @return annotation in the same hierarchy as qualifier, or null if one is not found */ - // This method requires more revision. - public abstract boolean isSubtypeTypeVariable( - Collection subAnnos, - Collection superAnnos); + default @Nullable AnnotationMirror findAnnotationInSameHierarchy( + Collection qualifiers, AnnotationMirror qualifier) { + AnnotationMirror top = this.getTopAnnotation(qualifier); + return findAnnotationInHierarchy(qualifiers, top); + } /** - * Returns the least upper bound for the qualifiers a1 and a2. - * - *

        Examples: - * - *

          - *
        • For NonNull, leastUpperBound('Nullable', 'NonNull') → Nullable - *
        - * - * The two qualifiers have to be from the same qualifier hierarchy. Otherwise, null will be - * returned. + * Returns the annotation in qualifiers that is in the hierarchy for which annotationMirror is + * top. * - *

        This method works even if the underlying Java type is a type variable. In that case, a - * 'null' AnnnotationMirror and the empty set represent a meaningful value (namely, no - * annotation). - * - * @return the least restrictive qualifiers for both types + * @param qualifiers set of annotations to search + * @param top the top annotation in the hierarchy to which the returned annotation belongs + * @return annotation in the same hierarchy as annotationMirror, or null if one is not found */ - public abstract AnnotationMirror leastUpperBoundTypeVariable( - AnnotationMirror a1, AnnotationMirror a2); + default @Nullable AnnotationMirror findAnnotationInHierarchy( + Collection qualifiers, AnnotationMirror top) { + for (AnnotationMirror anno : qualifiers) { + if (isSubtype(anno, top)) { + return anno; + } + } + return null; + } /** - * Returns the greatest lower bound for the qualifiers a1 and a2. - * - *

        The two qualifiers have to be from the same qualifier hierarchy. Otherwise, null will be - * returned. + * Update a mapping from {@code key} to a set of AnnotationMirrors. If {@code key} is not + * already in the map, then put it in the map with a value of a new set containing {@code + * qualifier}. If the map contains {@code key}, then add {@code qualifier} to the set to which + * {@code key} maps. If that set contains a qualifier in the same hierarchy as {@code + * qualifier}, then don't add it and return false. * - *

        This method works even if the underlying Java type is a type variable. In that case, a - * 'null' AnnnotationMirror and the empty set represent a meaningful value (namely, no - * annotation). - * - * @param a1 first annotation - * @param a2 second annotation - * @return greatest lower bound of the two annotations + * @param map the mapping to modify + * @param key the key to update or add + * @param qualifier the value to update or add + * @param type of the map's keys + * @return true if the update was done; false if there was a qualifier hierarchy collision */ - public abstract AnnotationMirror greatestLowerBoundTypeVariable( - AnnotationMirror a1, AnnotationMirror a2); + default boolean updateMappingToMutableSet( + Map> map, T key, AnnotationMirror qualifier) { + // https://github.com/typetools/checker-framework/issues/2000 + @SuppressWarnings("nullness:argument.type.incompatible") + boolean mapContainsKey = map.containsKey(key); + if (mapContainsKey) { + @SuppressWarnings("nullness:assignment.type.incompatible") // key is a key for map. + @NonNull Set prevs = map.get(key); + AnnotationMirror old = findAnnotationInSameHierarchy(prevs, qualifier); + if (old != null) { + return false; + } + prevs.add(qualifier); + map.put(key, prevs); + } else { + Set set = AnnotationUtils.createAnnotationSet(); + set.add(qualifier); + map.put(key, set); + } + return true; + } /** - * Returns the type qualifiers that are the least upper bound of the qualifiers in annos1 and - * annos2. - * - *

        This is necessary for determining the type of a conditional expression ({@code ?:}), where - * the type of the expression is the least upper bound of the true and false clauses. - * - *

        This method works even if the underlying Java type is a type variable. In that case, a - * 'null' AnnnotationMirror and the empty set represent a meaningful value (namely, no - * annotation). + * Throws an exception if the given collections do not have the same size. * - * @return the least upper bound of annos1 and annos2 + * @param c1 the first collection + * @param c2 the second collection */ - public Set leastUpperBoundsTypeVariable( - Collection annos1, - Collection annos2) { - Set result = AnnotationUtils.createAnnotationSet(); - for (AnnotationMirror top : getTopAnnotations()) { - AnnotationMirror anno1ForTop = null; - for (AnnotationMirror anno1 : annos1) { - if (isSubtypeTypeVariable(anno1, top)) { - anno1ForTop = anno1; - } - } - AnnotationMirror anno2ForTop = null; - for (AnnotationMirror anno2 : annos2) { - if (isSubtypeTypeVariable(anno2, top)) { - anno2ForTop = anno2; - } - } - AnnotationMirror t = leastUpperBoundTypeVariable(anno1ForTop, anno2ForTop); - if (t != null) { - result.add(t); - } + static void assertSameSize(Collection c1, Collection c2) { + if (c1.size() != c2.size()) { + throw new BugInCF( + "inconsistent sizes (%d, %d):%n %s%n %s", c1.size(), c2.size(), c1, c2); } - return result; } /** - * Returns the type qualifiers that are the greatest lower bound of the qualifiers in annos1 and - * annos2. - * - *

        The two qualifiers have to be from the same qualifier hierarchy. Otherwise, null will be - * returned. - * - *

        This method works even if the underlying Java type is a type variable. In that case, a - * 'null' AnnnotationMirror and the empty set represent a meaningful value (namely, no - * annotation). + * Throws an exception if the result does not have the same size as the inputs (which are + * assumed to have the same size as one another). * - * @param annos1 first collection of qualifiers - * @param annos2 second collection of qualifiers - * @return greatest lower bound of the two collections of qualifiers + * @param c1 the first collection + * @param c2 the second collection + * @param result the result collection */ - public Set greatestLowerBoundsTypeVariable( - Collection annos1, - Collection annos2) { - Set result = AnnotationUtils.createAnnotationSet(); - for (AnnotationMirror top : getTopAnnotations()) { - AnnotationMirror anno1ForTop = null; - for (AnnotationMirror anno1 : annos1) { - if (isSubtypeTypeVariable(anno1, top)) { - anno1ForTop = anno1; - } - } - AnnotationMirror anno2ForTop = null; - for (AnnotationMirror anno2 : annos2) { - if (isSubtypeTypeVariable(anno2, top)) { - anno2ForTop = anno2; - } - } - AnnotationMirror t = greatestLowerBoundTypeVariable(anno1ForTop, anno2ForTop); - if (t != null) { - result.add(t); - } + static void assertSameSize(Collection c1, Collection c2, Collection result) { + if (c1.size() != result.size()) { + throw new BugInCF( + "inconsistent sizes (%d, %d, %d):%n %s%n %s%n %s", + c1.size(), c2.size(), result.size(), c1, c2, result); } - return result; } + // ********************************************************************** + // Deprecated methods + // ********************************************************************** + /** - * Returns true if and only if the given type can have empty annotation sets (and thus the - * *TypeVariable methods need to be used). - */ - public static boolean canHaveEmptyAnnotationSet(AnnotatedTypeMirror type) { - return type.getKind() == TypeKind.TYPEVAR - || type.getKind() == TypeKind.WILDCARD - || - // TODO: or should the union/intersection be the LUB of the alternatives? - type.getKind() == TypeKind.UNION - || type.getKind() == TypeKind.INTERSECTION; + * Tests whether {@code subQualifier} is a sub-qualifier of, or equal to, {@code + * superQualifier}, according to the type qualifier hierarchy. + * + *

        This method works even if the underlying Java type is a type variable. In that case, a + * 'null' AnnotationMirror is a legal argument that represents no annotation. + * + * @param subQualifier a qualifier that might be a subtype + * @param superQualifier a qualifier that might be a subtype + * @return true iff {@code subQualifier} is a subqualifier of, or equal to, {@code + * superQualifier} + * @deprecated Without the bounds of the type variable, it is not possible to correctly compute + * the subtype relationship between "no qualifier" and a qualifier. Use {@link + * TypeHierarchy#isSubtype(AnnotatedTypeMirror, AnnotatedTypeMirror)}. + */ + @Deprecated + default boolean isSubtypeTypeVariable( + @Nullable AnnotationMirror subQualifier, @Nullable AnnotationMirror superQualifier) { + if (subQualifier == null) { + // [] is a supertype of any qualifier, and [] <: [] + return true; + } else if (superQualifier == null) { + // [] is a subtype of no qualifier (only []) + return false; + } + return isSubtype(subQualifier, superQualifier); } /** - * Tests whether {@code subAnno} is a sub-qualifier of, or equal to, {@code superAnno}, - * according to the type qualifier hierarchy. This checks only the qualifiers, not the Java - * type. + * Tests whether {@code subQualifier} is a sub-qualifier of, or equal to, {@code + * superQualifier}, according to the type qualifier hierarchy. This checks only the qualifiers, + * not the Java type. * *

        This method takes an annotated type to decide if the type variable version of the method * should be invoked, or if the normal version is sufficient (which provides more strict @@ -407,25 +425,30 @@ public static boolean canHaveEmptyAnnotationSet(AnnotatedTypeMirror type) { * * @param subType used to decide whether to call isSubtypeTypeVariable * @param superType used to decide whether to call isSubtypeTypeVariable - * @param subAnno the type qualifier that might be a subtype - * @param superAnno the type qualifier that might be a supertype - * @return true iff {@code subAnno} is a sub qualifier of, or equal to, {@code superAnno} - */ - public boolean isSubtype( + * @param subQualifier the type qualifier that might be a subtype + * @param superQualifier the type qualifier that might be a supertype + * @return true iff {@code subQualifier} is a subqualifier of, or equal to, {@code + * superQualifier} + * @deprecated Without the bounds of the type variable, it is not possible to correctly compute + * the subtype relationship between "no qualifier" and a qualifier. Use {@link + * TypeHierarchy#isSubtype(AnnotatedTypeMirror, AnnotatedTypeMirror)}. + */ + @Deprecated + default boolean isSubtype( AnnotatedTypeMirror subType, AnnotatedTypeMirror superType, - AnnotationMirror subAnno, - AnnotationMirror superAnno) { + AnnotationMirror subQualifier, + AnnotationMirror superQualifier) { if (canHaveEmptyAnnotationSet(subType) || canHaveEmptyAnnotationSet(superType)) { - return isSubtypeTypeVariable(subAnno, superAnno); + return isSubtypeTypeVariable(subQualifier, superQualifier); } else { - return isSubtype(subAnno, superAnno); + return isSubtype(subQualifier, superQualifier); } } /** - * Tests whether there is any annotation in {@code supers} that is a super qualifier of, or - * equal to, some annotation in {@code subs}. {@code supers} and {@code subs} contain only the + * Tests whether there is any annotation in {@code supers} that is a superqualifier of, or equal + * to, some annotation in {@code subs}. {@code supers} and {@code subs} contain only the * annotations, not the Java type. * *

        This method takes an annotated type to decide if the type variable version of the method @@ -438,8 +461,12 @@ public boolean isSubtype( * @param supers the type qualifiers that might be a supertype * @return true iff an annotation in {@code supers} is a supertype of, or equal to, one in * {@code subs} + * @deprecated Without the bounds of the type variable, it is not possible to correctly compute + * the subtype relationship between "no qualifier" and a qualifier. Use {@link + * TypeHierarchy#isSubtype(AnnotatedTypeMirror, AnnotatedTypeMirror)}. */ - public boolean isSubtype( + @Deprecated + default boolean isSubtype( AnnotatedTypeMirror subType, AnnotatedTypeMirror superType, Collection subs, @@ -452,7 +479,38 @@ public boolean isSubtype( } /** - * Returns the least upper bound for the qualifiers a1 and a2. + * Tests whether there is any annotation in superAnnos that is a superqualifier of or equal to + * some annotation in subAnnos. superAnnos and subAnnos contain only the annotations, not the + * Java type. + * + *

        This method works even if the underlying Java type is a type variable. In that case, the + * empty set is a legal argument that represents no annotation. + * + * @param subAnnos qualifiers + * @param superAnnos qualifiers + * @return true iff an annotation in superAnnos is a supertype of, or equal to, one in subAnnos + * @deprecated Without the bounds of the type variable, it is not possible to correctly compute + * the subtype relationship between "no qualifier" and a qualifier. Use {@link + * TypeHierarchy#isSubtype(AnnotatedTypeMirror, AnnotatedTypeMirror)}. + */ + // This method requires more revision. + @Deprecated + default boolean isSubtypeTypeVariable( + Collection subAnnos, + Collection superAnnos) { + for (AnnotationMirror top : getTopAnnotations()) { + AnnotationMirror rhsForTop = findAnnotationInHierarchy(subAnnos, top); + AnnotationMirror lhsForTop = findAnnotationInHierarchy(superAnnos, top); + if (!isSubtypeTypeVariable(rhsForTop, lhsForTop)) { + return false; + } + } + return true; + } + + /** + * Returns the least upper bound for the qualifiers a1 and a2. Returns null if the qualifiers + * are not from the same qualifier hierarchy. * *

        Examples: * @@ -460,50 +518,61 @@ public boolean isSubtype( *

      • For NonNull, leastUpperBound('Nullable', 'NonNull') → Nullable * * - * The two qualifiers have to be from the same qualifier hierarchy. Otherwise, null will be - * returned. - * - *

        This method takes an annotated type to decide if the type variable version of the method - * should be invoked, or if the normal version is sufficient (which provides more strict - * checks). + *

        This method works even if the underlying Java type is a type variable. In that case, a + * 'null' AnnotationMirror is a legal argument that represents no annotation. * + * @param a1 anno1 + * @param a2 anno2 * @return the least restrictive qualifiers for both types - */ - public AnnotationMirror leastUpperBound( - AnnotatedTypeMirror type1, - AnnotatedTypeMirror type2, - AnnotationMirror a1, - AnnotationMirror a2) { - if (canHaveEmptyAnnotationSet(type1) || canHaveEmptyAnnotationSet(type2)) { - return leastUpperBoundTypeVariable(a1, a2); - } else { - return leastUpperBound(a1, a2); + * @deprecated Without the bounds of the type variable, it is not possible to correctly compute + * the relationship between "no qualifier" and a qualifier. Use {@link + * org.checkerframework.framework.util.AnnotatedTypes#leastUpperBound(AnnotatedTypeFactory, + * AnnotatedTypeMirror, AnnotatedTypeMirror)}. + */ + @Deprecated + default @Nullable AnnotationMirror leastUpperBoundTypeVariable( + AnnotationMirror a1, AnnotationMirror a2) { + if (a1 == null || a2 == null) { + // [] is a supertype of any qualifier, and [] <: [] + return null; } + return leastUpperBound(a1, a2); } /** - * Returns the greatest lower bound for the qualifiers a1 and a2. + * Returns the least upper bound for the qualifiers a1 and a2. Returns null if the qualifiers + * are not from the same qualifier hierarchy. + * + *

        Examples: * - *

        The two qualifiers have to be from the same qualifier hierarchy. Otherwise, null will be - * returned. + *

          + *
        • For NonNull, leastUpperBound('Nullable', 'NonNull') → Nullable + *
        * *

        This method takes an annotated type to decide if the type variable version of the method * should be invoked, or if the normal version is sufficient (which provides more strict * checks). * - * @param a1 first annotation - * @param a2 second annotation - * @return greatest lower bound of the two annotations + * @param type1 type 1 + * @param type2 type 2 + * @param a1 annotation + * @param a2 annotation + * @return the least restrictive qualifiers for both types + * @deprecated Without the bounds of the type variable, it is not possible to correctly compute + * the relationship between "no qualifier" and a qualifier. Use {@link + * org.checkerframework.framework.util.AnnotatedTypes#leastUpperBound(AnnotatedTypeFactory, + * AnnotatedTypeMirror, AnnotatedTypeMirror)}. */ - public AnnotationMirror greatestLowerBound( + @Deprecated + default @Nullable AnnotationMirror leastUpperBound( AnnotatedTypeMirror type1, AnnotatedTypeMirror type2, AnnotationMirror a1, AnnotationMirror a2) { if (canHaveEmptyAnnotationSet(type1) || canHaveEmptyAnnotationSet(type2)) { - return greatestLowerBoundTypeVariable(a1, a2); + return leastUpperBoundTypeVariable(a1, a2); } else { - return greatestLowerBound(a1, a2); + return leastUpperBound(a1, a2); } } @@ -514,140 +583,200 @@ public AnnotationMirror greatestLowerBound( *

        This is necessary for determining the type of a conditional expression ({@code ?:}), where * the type of the expression is the least upper bound of the true and false clauses. * - *

        This method takes an annotated type to decide if the type variable version of the method - * should be invoked, or if the normal version is sufficient (which provides more strict - * checks). + *

        This method works even if the underlying Java type is a type variable. In that case, the + * empty set is a legal argument that represents no annotation. * + * @param annos1 qualifiers + * @param annos2 qualifiers * @return the least upper bound of annos1 and annos2 - */ - public Set leastUpperBounds( - AnnotatedTypeMirror type1, - AnnotatedTypeMirror type2, + * @deprecated Without the bounds of the type variable, it is not possible to correctly compute + * the relationship between "no qualifier" and a qualifier. Use {@link + * org.checkerframework.framework.util.AnnotatedTypes#leastUpperBound(AnnotatedTypeFactory, + * AnnotatedTypeMirror, AnnotatedTypeMirror)}. + */ + @Deprecated + @SuppressWarnings("nullness") // Don't check deprecated method. + default Set leastUpperBoundsTypeVariable( Collection annos1, - Collection annos2) { - if (canHaveEmptyAnnotationSet(type1) || canHaveEmptyAnnotationSet(type2)) { - return leastUpperBoundsTypeVariable(annos1, annos2); - } else { - return leastUpperBounds(annos1, annos2); + Collection annos2) { + Set result = AnnotationUtils.createAnnotationSet(); + for (AnnotationMirror top : getTopAnnotations()) { + AnnotationMirror anno1ForTop = null; + for (AnnotationMirror anno1 : annos1) { + if (isSubtypeTypeVariable(anno1, top)) { + anno1ForTop = anno1; + } + } + AnnotationMirror anno2ForTop = null; + for (AnnotationMirror anno2 : annos2) { + if (isSubtypeTypeVariable(anno2, top)) { + anno2ForTop = anno2; + } + } + AnnotationMirror t = leastUpperBoundTypeVariable(anno1ForTop, anno2ForTop); + if (t != null) { + result.add(t); + } } + return result; } /** - * Returns the type qualifiers that are the greatest lower bound of the qualifiers in annos1 and + * Returns the type qualifiers that are the least upper bound of the qualifiers in annos1 and * annos2. * - *

        The two qualifiers have to be from the same qualifier hierarchy. Otherwise, null will be - * returned. + *

        This is necessary for determining the type of a conditional expression ({@code ?:}), where + * the type of the expression is the least upper bound of the true and false clauses. * *

        This method takes an annotated type to decide if the type variable version of the method * should be invoked, or if the normal version is sufficient (which provides more strict * checks). * - * @param annos1 first collection of qualifiers - * @param annos2 second collection of qualifiers - * @return greatest lower bound of the two collections of qualifiers + * @param type1 annotated type + * @param type2 annotated type + * @param annos1 qualifiers + * @param annos2 qualifiers + * @return the least upper bound of annos1 and annos2 + * @deprecated Without the bounds of the type variable, it is not possible to correctly compute + * the relationship between "no qualifier" and a qualifier. Use {@link + * org.checkerframework.framework.util.AnnotatedTypes#leastUpperBound(AnnotatedTypeFactory, + * AnnotatedTypeMirror, AnnotatedTypeMirror)}. */ - public Set greatestLowerBounds( + @Deprecated + default Set leastUpperBounds( AnnotatedTypeMirror type1, AnnotatedTypeMirror type2, Collection annos1, Collection annos2) { if (canHaveEmptyAnnotationSet(type1) || canHaveEmptyAnnotationSet(type2)) { - return greatestLowerBoundsTypeVariable(annos1, annos2); + return leastUpperBoundsTypeVariable(annos1, annos2); } else { - return greatestLowerBounds(annos1, annos2); + return leastUpperBounds(annos1, annos2); } } /** - * Returns the annotation in annos that is in the same hierarchy as annotationMirror. + * Returns the greatest lower bound for the qualifiers a1 and a2. Returns null if the qualifiers + * are not from the same qualifier hierarchy. * - * @param annos set of annotations to search - * @param annotationMirror annotation that is in the same hierarchy as the returned annotation - * @return annotation in the same hierarchy as annotationMirror, or null if one is not found - */ - public AnnotationMirror findAnnotationInSameHierarchy( - Collection annos, AnnotationMirror annotationMirror) { - AnnotationMirror top = this.getTopAnnotation(annotationMirror); - return findAnnotationInHierarchy(annos, top); - } - - /** - * Returns the annotation in annos that is in the hierarchy for which annotationMirror is top. + *

        This method works even if the underlying Java type is a type variable. In that case, a + * 'null' AnnotationMirror is a legal argument that represents no annotation. * - * @param annos set of annotations to search - * @param top the top annotation in the hierarchy to which the returned annotation belongs - * @return annotation in the same hierarchy as annotationMirror, or null if one is not found - */ - public AnnotationMirror findAnnotationInHierarchy( - Collection annos, AnnotationMirror top) { - for (AnnotationMirror anno : annos) { - if (isSubtype(anno, top)) { - return anno; - } + * @param a1 first annotation + * @param a2 second annotation + * @return greatest lower bound of the two annotations + * @deprecated Without the bounds of the type variable, it is not possible to correctly compute + * the relationship between "no qualifier" and a qualifier + */ + @Deprecated + default @Nullable AnnotationMirror greatestLowerBoundTypeVariable( + AnnotationMirror a1, AnnotationMirror a2) { + if (a1 == null) { + // [] is a supertype of any qualifier, and [] <: [] + return a2; } - return null; + if (a2 == null) { + // [] is a supertype of any qualifier, and [] <: [] + return a1; + } + return greatestLowerBound(a1, a2); } /** - * Update a mapping from some key to a set of AnnotationMirrors. If the key already exists in - * the mapping and the new qualifier is in the same qualifier hierarchy as any of the existing - * qualifiers, do nothing and return false. If the key already exists in the mapping and the new - * qualifier is not in the same qualifier hierarchy as any of the existing qualifiers, add the - * qualifier to the existing set and return true. If the key does not exist in the mapping, add - * the new qualifier as a singleton set and return true. + * Returns the type qualifiers that are the greatest lower bound of the qualifiers in annos1 and + * annos2. Returns null if the qualifiers are not from the same qualifier hierarchy. * - * @param map the mapping to modify - * @param key the key to update - * @param newQual the value to add - * @return true if the update was done; false if there was a qualifier hierarchy collision + *

        This method works even if the underlying Java type is a type variable. In that case, the + * empty set is a legal argument that represents no annotation. + * + * @param annos1 first collection of qualifiers + * @param annos2 second collection of qualifiers + * @return greatest lower bound of the two collections of qualifiers + * @deprecated Without the bounds of the type variable, it is not possible to correctly compute + * the relationship between "no qualifier" and a qualifier */ - public boolean updateMappingToMutableSet( - Map> map, T key, AnnotationMirror newQual) { - - if (!map.containsKey(key)) { - Set set = AnnotationUtils.createAnnotationSet(); - set.add(newQual); - map.put(key, set); - } else { - Set prevs = map.get(key); - for (AnnotationMirror p : prevs) { - if (AnnotationUtils.areSame(getTopAnnotation(p), getTopAnnotation(newQual))) { - return false; + @Deprecated + @SuppressWarnings("nullness") // Don't check deprecated method. + default Set greatestLowerBoundsTypeVariable( + Collection annos1, + Collection annos2) { + Set result = AnnotationUtils.createAnnotationSet(); + for (AnnotationMirror top : getTopAnnotations()) { + AnnotationMirror anno1ForTop = null; + for (AnnotationMirror anno1 : annos1) { + if (isSubtypeTypeVariable(anno1, top)) { + anno1ForTop = anno1; } } - prevs.add(newQual); - map.put(key, prevs); + AnnotationMirror anno2ForTop = null; + for (AnnotationMirror anno2 : annos2) { + if (isSubtypeTypeVariable(anno2, top)) { + anno2ForTop = anno2; + } + } + AnnotationMirror t = greatestLowerBoundTypeVariable(anno1ForTop, anno2ForTop); + if (t != null) { + result.add(t); + } } - return true; + return result; } /** - * Throws an exception if the given collections do not have the same size. + * Returns the greatest lower bound for the qualifiers a1 and a2. Returns null if the qualifiers + * are not from the same qualifier hierarchy. * - * @param c1 the first collection - * @param c2 the second collection + *

        This method takes an annotated type to decide if the type variable version of the method + * should be invoked, or if the normal version is sufficient (which provides more strict + * checks). + * + * @param type1 annotated type + * @param type2 annotated type + * @param a1 first annotation + * @param a2 second annotation + * @return greatest lower bound of the two annotations + * @deprecated Without the bounds of the type variable, it is not possible to correctly compute + * the relationship between "no qualifier" and a qualifier */ - private static void assertSameSize(Collection c1, Collection c2) { - if (c1.size() != c2.size()) { - throw new BugInCF( - "inconsistent sizes (%d, %d):%n %s%n %s", c1.size(), c2.size(), c1, c2); + @Deprecated + default @Nullable AnnotationMirror greatestLowerBound( + AnnotatedTypeMirror type1, + AnnotatedTypeMirror type2, + AnnotationMirror a1, + AnnotationMirror a2) { + if (canHaveEmptyAnnotationSet(type1) || canHaveEmptyAnnotationSet(type2)) { + return greatestLowerBoundTypeVariable(a1, a2); + } else { + return greatestLowerBound(a1, a2); } } /** - * Throws an exception if the result does not have the same size as the inputs (which are - * assumed to have the same size as one another). + * Returns the type qualifiers that are the greatest lower bound of the qualifiers in annos1 and + * annos2. Returns null if the qualifiers are not from the same qualifier hierarchy. * - * @param c1 the first collection - * @param c2 the second collection - * @param result the result collection + *

        This method takes an annotated type to decide if the type variable version of the method + * should be invoked, or if the normal version is sufficient (which provides more strict + * checks). + * + * @param type1 annotated type + * @param type2 annotated type + * @param annos1 first collection of qualifiers + * @param annos2 second collection of qualifiers + * @return greatest lower bound of the two collections of qualifiers + * @deprecated Without the bounds of the type variable, it is not possible to correctly compute + * the relationship between "no qualifier" and a qualifier */ - private static void assertSameSize(Collection c1, Collection c2, Collection result) { - if (c1.size() != result.size()) { - throw new BugInCF( - "inconsistent sizes (%d, %d, %d):%n %s%n %s%n %s", - c1.size(), c2.size(), result.size(), c1, c2, result); + @Deprecated + default Set greatestLowerBounds( + AnnotatedTypeMirror type1, + AnnotatedTypeMirror type2, + Collection annos1, + Collection annos2) { + if (canHaveEmptyAnnotationSet(type1) || canHaveEmptyAnnotationSet(type2)) { + return greatestLowerBoundsTypeVariable(annos1, annos2); + } else { + return greatestLowerBounds(annos1, annos2); } } } diff --git a/framework/src/main/java/org/checkerframework/framework/type/StructuralEqualityComparer.java b/framework/src/main/java/org/checkerframework/framework/type/StructuralEqualityComparer.java index 573d4b93ced..9567bd1ae5b 100644 --- a/framework/src/main/java/org/checkerframework/framework/type/StructuralEqualityComparer.java +++ b/framework/src/main/java/org/checkerframework/framework/type/StructuralEqualityComparer.java @@ -6,6 +6,7 @@ import javax.lang.model.element.AnnotationMirror; import javax.lang.model.type.TypeKind; import javax.lang.model.util.Types; +import org.checkerframework.checker.interning.qual.EqualsMethod; import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedArrayType; import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedDeclaredType; import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedIntersectionType; @@ -89,13 +90,16 @@ protected String defaultErrorMessage( * Framework sometimes "infers" Typevars to be Wildcards, we allow the combination * Wildcard,Typevar. In this case, the two types are "equal" if their bounds are. * + * @param type1 the first AnnotatedTypeMirror to compare + * @param type2 the second AnnotatedTypeMirror to compare * @return true if type1 and type2 are equal */ + @EqualsMethod private boolean areEqual(final AnnotatedTypeMirror type1, final AnnotatedTypeMirror type2) { - assert currentTop != null; if (type1 == type2) { return true; } + assert currentTop != null; if (type1 == null || type2 == null) { return false; } diff --git a/framework/src/main/java/org/checkerframework/framework/type/SubtypeVisitHistory.java b/framework/src/main/java/org/checkerframework/framework/type/SubtypeVisitHistory.java index d9dd6f32ee3..daf262545f1 100644 --- a/framework/src/main/java/org/checkerframework/framework/type/SubtypeVisitHistory.java +++ b/framework/src/main/java/org/checkerframework/framework/type/SubtypeVisitHistory.java @@ -11,9 +11,10 @@ * THIS CLASS IS DESIGNED FOR USE WITH DefaultTypeHierarchy, DefaultRawnessComparer, and * StructuralEqualityComparer ONLY. * - *

        VisitHistory keeps track of all visits and allows clients of this class to check whether or - * not they have visited an equivalent pair of AnnotatedTypeMirrors already. This is necessary in - * order to halt visiting on recursive bounds. + *

        VisitHistory tracks triples of (type1, type2, top), where type1 is a subtype of type2. It does + * not track when type1 is not a subtype of type2; such entries are missing from the history. + * Clients of this class can check whether or not they have visited an equivalent pair of + * AnnotatedTypeMirrors already. This is necessary in order to halt visiting on recursive bounds. * *

        This class is primarily used to implement isSubtype(ATM, ATM). The pair of types corresponds * to the subtype and the supertype being checked. A single subtype may be visited more than once, @@ -35,12 +36,20 @@ public SubtypeVisitHistory() { this.visited = new HashMap<>(); } - /** Add a visit for type1 and type2. */ + /** + * Add a visit for type1 and type2. Has no effect if b is false. + * + * @param type1 the first type + * @param type2 the second type + * @param currentTop the top of the relevant type hierarchy; only annotations from that + * hierarchy are considered + * @param b true if type1 is a subtype of type2; if false, this method does nothing + */ public void add( final AnnotatedTypeMirror type1, final AnnotatedTypeMirror type2, AnnotationMirror currentTop, - Boolean b) { + boolean b) { if (!b) { // We only store information about subtype relations that hold. return; diff --git a/framework/src/main/java/org/checkerframework/framework/type/SupertypeFinder.java b/framework/src/main/java/org/checkerframework/framework/type/SupertypeFinder.java index efc1dcfef33..0ed4ba9efc9 100644 --- a/framework/src/main/java/org/checkerframework/framework/type/SupertypeFinder.java +++ b/framework/src/main/java/org/checkerframework/framework/type/SupertypeFinder.java @@ -32,8 +32,9 @@ import org.checkerframework.javacutil.TypesUtils; /** - * Finds the direct supertypes of an input AnnotatedTypeMirror. See - * https://docs.oracle.com/javase/specs/jls/se11/html/jls-4.html#jls-4.10.2 + * Finds the direct supertypes of an input AnnotatedTypeMirror. See JLS section + * 4.10.2. * * @see Types#directSupertypes(TypeMirror) */ diff --git a/framework/src/main/java/org/checkerframework/framework/type/TypeFromTypeTreeVisitor.java b/framework/src/main/java/org/checkerframework/framework/type/TypeFromTypeTreeVisitor.java index 7d609363c81..69116daf57f 100644 --- a/framework/src/main/java/org/checkerframework/framework/type/TypeFromTypeTreeVisitor.java +++ b/framework/src/main/java/org/checkerframework/framework/type/TypeFromTypeTreeVisitor.java @@ -32,6 +32,7 @@ import javax.lang.model.element.TypeParameterElement; import javax.lang.model.type.TypeKind; import javax.lang.model.type.TypeVariable; +import org.checkerframework.checker.interning.qual.FindDistinct; import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedArrayType; import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedDeclaredType; import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedIntersectionType; @@ -150,10 +151,14 @@ public AnnotatedTypeMirror visitParameterizedType( *

        In scenarios where the bound's owner is the same, we don't want to replace a * capture-converted bound in the wildcard type with a non-capture-converted bound given by the * type parameter declaration. + * + * @param typeArgs the type of the arguments at (e.g., at the call side) + * @param typeParams the type of the formal parameters (e.g., at the method declaration) */ + @SuppressWarnings("interning:not.interned") // workaround for javac bug private void updateWildcardBounds( List typeArgs, List typeParams) { - if (typeArgs.size() == 0) { + if (typeArgs.isEmpty()) { // Nothing to do for empty type arguments. return; } @@ -184,7 +189,7 @@ public AnnotatedTypeMirror visitPrimitiveType(PrimitiveTypeTree node, AnnotatedT @Override public AnnotatedTypeVariable visitTypeParameter( - TypeParameterTree node, AnnotatedTypeFactory f) { + TypeParameterTree node, @FindDistinct AnnotatedTypeFactory f) { List bounds = new ArrayList<>(node.getBounds().size()); for (Tree t : node.getBounds()) { diff --git a/framework/src/main/java/org/checkerframework/framework/type/TypeHierarchy.java b/framework/src/main/java/org/checkerframework/framework/type/TypeHierarchy.java index e1f932f818a..3b36452e56a 100644 --- a/framework/src/main/java/org/checkerframework/framework/type/TypeHierarchy.java +++ b/framework/src/main/java/org/checkerframework/framework/type/TypeHierarchy.java @@ -2,7 +2,7 @@ import org.checkerframework.framework.util.AnnotatedTypes; -/** Compares AnnotatedTypeMirrors for subtype relationships. See also QualifierHierarchy */ +/** Compares AnnotatedTypeMirrors for subtype relationships. See also {@link QualifierHierarchy}. */ public interface TypeHierarchy { /** diff --git a/framework/src/main/java/org/checkerframework/framework/type/poly/AbstractQualifierPolymorphism.java b/framework/src/main/java/org/checkerframework/framework/type/poly/AbstractQualifierPolymorphism.java index 56a1cb84ec5..085c63ed7d0 100644 --- a/framework/src/main/java/org/checkerframework/framework/type/poly/AbstractQualifierPolymorphism.java +++ b/framework/src/main/java/org/checkerframework/framework/type/poly/AbstractQualifierPolymorphism.java @@ -248,7 +248,7 @@ public void resolve( public void resolve( AnnotatedExecutableType functionalInterface, AnnotatedExecutableType memberReference) { for (AnnotationMirror type : functionalInterface.getReturnType().getAnnotations()) { - if (QualifierPolymorphism.hasPolymorphicQualifier(type)) { + if (atypeFactory.getQualifierHierarchy().isPolymorphicQualifier(type)) { // functional interface has a polymorphic qualifier, so they should not be resolved // on memberReference. return; diff --git a/framework/src/main/java/org/checkerframework/framework/type/poly/DefaultQualifierPolymorphism.java b/framework/src/main/java/org/checkerframework/framework/type/poly/DefaultQualifierPolymorphism.java index b4f2afa7c96..f167f9a5fa4 100644 --- a/framework/src/main/java/org/checkerframework/framework/type/poly/DefaultQualifierPolymorphism.java +++ b/framework/src/main/java/org/checkerframework/framework/type/poly/DefaultQualifierPolymorphism.java @@ -59,6 +59,6 @@ protected AnnotationMirror combine( } else if (a2 == null) { return a1; } - return qualHierarchy.leastUpperBoundTypeVariable(a1, a2); + return qualHierarchy.leastUpperBound(a1, a2); } } diff --git a/framework/src/main/java/org/checkerframework/framework/type/poly/QualifierPolymorphism.java b/framework/src/main/java/org/checkerframework/framework/type/poly/QualifierPolymorphism.java index e1d7a402677..bb82ef17dc3 100644 --- a/framework/src/main/java/org/checkerframework/framework/type/poly/QualifierPolymorphism.java +++ b/framework/src/main/java/org/checkerframework/framework/type/poly/QualifierPolymorphism.java @@ -2,14 +2,10 @@ import com.sun.source.tree.MethodInvocationTree; import com.sun.source.tree.NewClassTree; -import javax.lang.model.element.AnnotationMirror; -import javax.lang.model.element.Element; -import javax.lang.model.element.Name; import javax.lang.model.element.VariableElement; import org.checkerframework.framework.qual.PolymorphicQualifier; import org.checkerframework.framework.type.AnnotatedTypeMirror; import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedExecutableType; -import org.checkerframework.javacutil.AnnotationUtils; /** * Interface to implement qualifier polymorphism. @@ -20,63 +16,6 @@ */ public interface QualifierPolymorphism { - /** - * Returns the {@link PolymorphicQualifier} meta-annotation on {@code qual} if one exists; - * otherwise return null. - * - * @return the {@link PolymorphicQualifier} meta-annotation on {@code qual} if one exists; - * otherwise return null - */ - static AnnotationMirror getPolymorphicQualifier(AnnotationMirror qual) { - if (qual == null) { - return null; - } - Element qualElt = qual.getAnnotationType().asElement(); - for (AnnotationMirror am : qualElt.getAnnotationMirrors()) { - if (AnnotationUtils.areSameByClass(am, PolymorphicQualifier.class)) { - return am; - } - } - return null; - } - - /** - * Returns true if {@code qual} has the {@link PolymorphicQualifier} meta-annotation. - * - * @param qual an annotation - * @return true if {@code qual} has the {@link PolymorphicQualifier} meta-annotation - */ - static boolean hasPolymorphicQualifier(AnnotationMirror qual) { - return getPolymorphicQualifier(qual) != null; - } - - /** - * If {@code qual} is a polymorphic qualifier, return the class specified by the {@link - * PolymorphicQualifier} meta-annotation on the polymorphic qualifier is returned. Otherwise, - * return null. - * - *

        This value identifies the qualifier hierarchy to which this polymorphic qualifier belongs. - * By convention, it is the top qualifier of the hierarchy. Use of {@code - * PolymorphicQualifier.class} is discouraged, because it can lead to ambiguity if used for - * multiple type systems. - * - * @param qual an annotation - * @return the class specified by the {@link PolymorphicQualifier} meta-annotation on {@code - * qual}, if {@code qual} is a polymorphic qualifier; otherwise, null. - * @see org.checkerframework.framework.qual.PolymorphicQualifier#value() - */ - static Name getPolymorphicQualifierElement(AnnotationMirror qual) { - AnnotationMirror poly = getPolymorphicQualifier(qual); - - // System.out.println("poly: " + poly + " pq: " + - // PolymorphicQualifier.class.getCanonicalName()); - if (poly == null) { - return null; - } - Name ret = AnnotationUtils.getElementValueClassName(poly, "value", true); - return ret; - } - /** * Resolves polymorphism annotations for the given type. * diff --git a/framework/src/main/java/org/checkerframework/framework/type/treeannotator/PropagationTreeAnnotator.java b/framework/src/main/java/org/checkerframework/framework/type/treeannotator/PropagationTreeAnnotator.java index 67f75e01e55..0e26f34d25c 100644 --- a/framework/src/main/java/org/checkerframework/framework/type/treeannotator/PropagationTreeAnnotator.java +++ b/framework/src/main/java/org/checkerframework/framework/type/treeannotator/PropagationTreeAnnotator.java @@ -7,7 +7,6 @@ import com.sun.source.tree.Tree; import com.sun.source.tree.TypeCastTree; import com.sun.source.tree.UnaryTree; -import java.util.Collection; import java.util.Set; import javax.lang.model.element.AnnotationMirror; import javax.lang.model.type.TypeKind; @@ -15,6 +14,7 @@ import org.checkerframework.framework.type.AnnotatedTypeMirror; import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedArrayType; import org.checkerframework.framework.type.QualifierHierarchy; +import org.checkerframework.javacutil.AnnotationUtils; import org.checkerframework.javacutil.Pair; /** @@ -47,7 +47,7 @@ public Void visitNewArray(NewArrayTree tree, AnnotatedTypeMirror type) { AnnotatedTypeMirror componentType = ((AnnotatedArrayType) type).getComponentType(); // prev is the lub of the initializers if they exist, otherwise the current component type. - Collection prev = null; + Set prev = null; if (tree.getInitializers() != null && !tree.getInitializers().isEmpty()) { // We have initializers, either with or without an array type. @@ -56,7 +56,7 @@ public Void visitNewArray(NewArrayTree tree, AnnotatedTypeMirror type) { for (ExpressionTree init : tree.getInitializers()) { AnnotatedTypeMirror initType = atypeFactory.getAnnotatedType(init); // initType might be a typeVariable, so use effectiveAnnotations. - Collection annos = initType.getEffectiveAnnotations(); + Set annos = initType.getEffectiveAnnotations(); prev = (prev == null) ? annos : qualHierarchy.leastUpperBounds(prev, annos); } @@ -69,7 +69,7 @@ public Void visitNewArray(NewArrayTree tree, AnnotatedTypeMirror type) { Pair context = atypeFactory.getVisitorState().getAssignmentContext(); - Collection post; + Set post; if (context != null && context.second != null @@ -106,7 +106,7 @@ public Void visitNewArray(NewArrayTree tree, AnnotatedTypeMirror type) { } // TODO (issue #599): This only works at the top level. It should work at all levels of // the array. - componentType.addMissingAnnotations(post); + addAnnoOrBound(componentType, post); return null; } @@ -190,8 +190,13 @@ public Void visitTypeCast(TypeCastTree node, AnnotatedTypeMirror type) { } else { // Use effective annotations from the expression, to get upper bound // of type variables. - type.addMissingAnnotations(exprType.getEffectiveAnnotations()); + Set expressionAnnos = exprType.getEffectiveAnnotations(); + // If the qualifier on the expression type is a supertype of the qualifier upper bound + // of the cast type, then apply the bound as the default qualifier rather than the + // expression qualifier. + addAnnoOrBound(type, expressionAnnos); } + return null; } @@ -204,4 +209,27 @@ private boolean hasPrimaryAnnotationInAllHierarchies(AnnotatedTypeMirror type) { } return annotated; } + + /** + * Adds the qualifiers in {@code annos} to {@code type} that are below the qualifier upper bound + * of type and for which type does not already have annotation in the same hierarchy. If a + * qualifier in {@code annos} is above the bound, then the bound is added to {@code type} + * instead. + * + * @param type annotations are added to this type + * @param annos annotations to add to type + */ + private void addAnnoOrBound(AnnotatedTypeMirror type, Set annos) { + Set boundAnnos = + atypeFactory.getQualifierUpperBounds().getBoundQualifiers(type.getUnderlyingType()); + Set annosToAdd = AnnotationUtils.createAnnotationSet(); + for (AnnotationMirror boundAnno : boundAnnos) { + AnnotationMirror anno = qualHierarchy.findAnnotationInSameHierarchy(annos, boundAnno); + if (anno != null && !qualHierarchy.isSubtype(anno, boundAnno)) { + annosToAdd.add(boundAnno); + } + } + type.addMissingAnnotations(annosToAdd); + type.addMissingAnnotations(annos); + } } diff --git a/framework/src/main/java/org/checkerframework/framework/type/typeannotator/PropagationTypeAnnotator.java b/framework/src/main/java/org/checkerframework/framework/type/typeannotator/PropagationTypeAnnotator.java index 2d4efb1f23e..95bcd4782bb 100644 --- a/framework/src/main/java/org/checkerframework/framework/type/typeannotator/PropagationTypeAnnotator.java +++ b/framework/src/main/java/org/checkerframework/framework/type/typeannotator/PropagationTypeAnnotator.java @@ -7,6 +7,7 @@ import javax.lang.model.element.Element; import javax.lang.model.element.TypeElement; import javax.lang.model.type.TypeKind; +import org.checkerframework.checker.interning.qual.FindDistinct; import org.checkerframework.framework.type.AnnotatedTypeFactory; import org.checkerframework.framework.type.AnnotatedTypeMirror; import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedDeclaredType; @@ -177,9 +178,14 @@ private void applyAnnosFromBound( * Search parent's type arguments for wildcard. Using the index of wildcard, find the * corresponding type parameter element and return it. Returns null if the wildcard is the * result of substitution and therefore not in the list of type arguments. + * + * @param wildcard the wildcard type whose corresponding type argument to determine + * @param parent the type that may have a type argument corresponding to {@code wildcard} + * @return the type argument in {@code parent} that corresponds to {@code wildcard} */ private Element getTypeParamFromEnclosingClass( - final AnnotatedWildcardType wildcard, final AnnotatedDeclaredType parent) { + final @FindDistinct AnnotatedWildcardType wildcard, + final AnnotatedDeclaredType parent) { Integer wildcardIndex = null; int currentIndex = 0; for (AnnotatedTypeMirror typeArg : parent.getTypeArguments()) { diff --git a/framework/src/main/java/org/checkerframework/framework/type/visitor/AnnotatedTypeCombiner.java b/framework/src/main/java/org/checkerframework/framework/type/visitor/AnnotatedTypeCombiner.java index 0d56d19b890..5551be9f987 100644 --- a/framework/src/main/java/org/checkerframework/framework/type/visitor/AnnotatedTypeCombiner.java +++ b/framework/src/main/java/org/checkerframework/framework/type/visitor/AnnotatedTypeCombiner.java @@ -20,6 +20,7 @@ public class AnnotatedTypeCombiner extends AnnotatedTypeComparer { * @param to the annotated type mirror into which annotations should be combined * @param hierarchy the top type of the hierarchy whose annotations should be combined */ + @SuppressWarnings("interning:not.interned") // assertion public static void combine( final AnnotatedTypeMirror from, final AnnotatedTypeMirror to, @@ -43,6 +44,7 @@ private AnnotatedTypeCombiner(final QualifierHierarchy hierarchy) { } @Override + @SuppressWarnings("interning:not.interned") // assertion protected Void compare(AnnotatedTypeMirror one, AnnotatedTypeMirror two) { assert one != two; if (one != null && two != null) { diff --git a/framework/src/main/java/org/checkerframework/framework/util/AnnotatedTypes.java b/framework/src/main/java/org/checkerframework/framework/util/AnnotatedTypes.java index b8ddaf099d1..01adda1d962 100644 --- a/framework/src/main/java/org/checkerframework/framework/util/AnnotatedTypes.java +++ b/framework/src/main/java/org/checkerframework/framework/util/AnnotatedTypes.java @@ -45,7 +45,6 @@ import org.checkerframework.framework.type.AsSuperVisitor; import org.checkerframework.framework.type.QualifierHierarchy; import org.checkerframework.framework.type.SyntheticArrays; -import org.checkerframework.framework.type.poly.QualifierPolymorphism; import org.checkerframework.javacutil.AnnotationUtils; import org.checkerframework.javacutil.BugInCF; import org.checkerframework.javacutil.ElementUtils; @@ -1410,7 +1409,7 @@ public static void copyOnlyExplicitConstructorAnnotations( // Collect all polymorphic qualifiers; we should substitute them. Set polys = AnnotationUtils.createAnnotationSet(); for (AnnotationMirror anno : returnType.getAnnotations()) { - if (QualifierPolymorphism.hasPolymorphicQualifier(anno)) { + if (atypeFactory.getQualifierHierarchy().isPolymorphicQualifier(anno)) { polys.add(anno); } } diff --git a/framework/src/main/java/org/checkerframework/framework/util/AtmLubVisitor.java b/framework/src/main/java/org/checkerframework/framework/util/AtmLubVisitor.java index 18e20476317..2b640b8c393 100644 --- a/framework/src/main/java/org/checkerframework/framework/util/AtmLubVisitor.java +++ b/framework/src/main/java/org/checkerframework/framework/util/AtmLubVisitor.java @@ -7,6 +7,7 @@ import javax.lang.model.type.TypeKind; import javax.lang.model.type.TypeMirror; import javax.lang.model.type.TypeVariable; +import org.checkerframework.checker.interning.qual.FindDistinct; import org.checkerframework.framework.type.AnnotatedTypeFactory; import org.checkerframework.framework.type.AnnotatedTypeMirror; import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedArrayType; @@ -244,13 +245,12 @@ private void lubWildcard( visit(type1LowerBound, type2LowerBound, lubLowerBound); for (AnnotationMirror top : qualifierHierarchy.getTopAnnotations()) { - AnnotationMirror glb = - qualifierHierarchy.greatestLowerBound( - type1LowerBound, - type2LowerBound, - type1LowerBound.getAnnotationInHierarchy(top), - type2LowerBound.getAnnotationInHierarchy(top)); - if (glb != null) { + AnnotationMirror anno1 = type1LowerBound.getAnnotationInHierarchy(top); + AnnotationMirror anno2 = type2LowerBound.getAnnotationInHierarchy(top); + + AnnotationMirror glb = null; + if (anno1 != null && anno2 != null) { + glb = qualifierHierarchy.greatestLowerBound(anno1, anno2); lubLowerBound.replaceAnnotation(glb); } } @@ -371,8 +371,11 @@ protected String defaultErrorMessage( * Returns true if the {@link AnnotatedTypeMirror} has been visited. If it has not, then it is * added to the list of visited AnnotatedTypeMirrors. This prevents infinite recursion on * recursive types. + * + * @param atm the type that might have been visited + * @return true if the given type has been visited */ - private boolean visited(AnnotatedTypeMirror atm) { + private boolean visited(@FindDistinct AnnotatedTypeMirror atm) { for (AnnotatedTypeMirror atmVisit : visited) { // Use reference equality rather than equals because the visitor may visit two types // that are structurally equal, but not actually the same. For example, the diff --git a/framework/src/main/java/org/checkerframework/framework/util/ContractsUtils.java b/framework/src/main/java/org/checkerframework/framework/util/ContractsUtils.java index a599539f9c4..55de6eaf255 100644 --- a/framework/src/main/java/org/checkerframework/framework/util/ContractsUtils.java +++ b/framework/src/main/java/org/checkerframework/framework/util/ContractsUtils.java @@ -11,6 +11,7 @@ import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.Name; import javax.lang.model.util.ElementFilter; +import org.checkerframework.checker.interning.qual.InternedDistinct; import org.checkerframework.framework.qual.ConditionalPostconditionAnnotation; import org.checkerframework.framework.qual.EnsuresQualifier; import org.checkerframework.framework.qual.EnsuresQualifierIf; @@ -42,7 +43,7 @@ public class ContractsUtils { * The currently-used ContractsUtils object. This class is NOT a singleton: this value can * change. */ - protected static ContractsUtils instance; + protected static @InternedDistinct ContractsUtils instance; /** The factory that this ContractsUtils is associated with. */ protected GenericAnnotatedTypeFactory factory; @@ -52,7 +53,14 @@ private ContractsUtils(GenericAnnotatedTypeFactory factory) { this.factory = factory; } - /** Returns an instance of the {@link ContractsUtils} class. */ + /** + * Returns an instance of the {@link ContractsUtils} class for the given factory. Also sets it + * as the currently-used ContractsUtils object. + * + * @param factory the factory to create a ContractsUtils for + * @return a ContractsUtils for the given factory + */ + @SuppressWarnings("interning") public static ContractsUtils getInstance(GenericAnnotatedTypeFactory factory) { if (instance == null || instance.factory != factory) { instance = new ContractsUtils(factory); diff --git a/framework/src/main/java/org/checkerframework/framework/util/DefaultAnnotationFormatter.java b/framework/src/main/java/org/checkerframework/framework/util/DefaultAnnotationFormatter.java index 77fd03e92f5..90676eebbae 100644 --- a/framework/src/main/java/org/checkerframework/framework/util/DefaultAnnotationFormatter.java +++ b/framework/src/main/java/org/checkerframework/framework/util/DefaultAnnotationFormatter.java @@ -12,7 +12,7 @@ import org.checkerframework.framework.qual.InvisibleQualifier; import org.checkerframework.javacutil.BugInCF; -/** A utility for converting AnnotationMirrors to Strings. */ +/** A utility for converting AnnotationMirrors to Strings. It omits full package names. */ public class DefaultAnnotationFormatter implements AnnotationFormatter { /** diff --git a/framework/src/main/java/org/checkerframework/framework/util/Heuristics.java b/framework/src/main/java/org/checkerframework/framework/util/Heuristics.java index e898f4afda6..cc3b7949f68 100644 --- a/framework/src/main/java/org/checkerframework/framework/util/Heuristics.java +++ b/framework/src/main/java/org/checkerframework/framework/util/Heuristics.java @@ -76,6 +76,12 @@ public Boolean visitParenthesized(ParenthesizedTree node, Void p) { return visit(node.getExpression(), p); } + /** + * Returns true if the given path matches this Matcher. + * + * @param path the path to test + * @return true if the given path matches this Matcher + */ public boolean match(TreePath path) { return visit(path.getLeaf(), null); } @@ -88,6 +94,7 @@ public PreceededBy(Matcher matcher) { this.matcher = matcher; } + @SuppressWarnings("interning:not.interned") @Override public boolean match(TreePath path) { StatementTree stmt = TreeUtils.enclosingOfClass(path, StatementTree.class); @@ -126,6 +133,9 @@ public boolean match(TreePath path) { * the leaf of a path, ignoring all other parts of it. */ public static class Within extends Matcher { + /** + * The matcher that {@code Within.match} will try, on every parent of the path it is given. + */ private final Matcher matcher; /** @@ -164,6 +174,7 @@ public WithinTrueBranch(Matcher conditionMatcher) { this.matcher = conditionMatcher; } + @SuppressWarnings("interning:not.interned") @Override public boolean match(TreePath path) { TreePath prev = path, p = path.getParentPath(); diff --git a/framework/src/main/java/org/checkerframework/framework/util/MultiGraphQualifierHierarchy.java b/framework/src/main/java/org/checkerframework/framework/util/MultiGraphQualifierHierarchy.java index c15f6d8573b..76fcee6f3bb 100644 --- a/framework/src/main/java/org/checkerframework/framework/util/MultiGraphQualifierHierarchy.java +++ b/framework/src/main/java/org/checkerframework/framework/util/MultiGraphQualifierHierarchy.java @@ -1,5 +1,6 @@ package org.checkerframework.framework.util; +import java.lang.annotation.Annotation; import java.util.Collection; import java.util.Collections; import java.util.HashMap; @@ -9,6 +10,7 @@ import java.util.Set; import java.util.StringJoiner; import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.Element; import javax.lang.model.element.Name; import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.dataflow.qual.Pure; @@ -16,7 +18,6 @@ import org.checkerframework.framework.qual.PolymorphicQualifier; import org.checkerframework.framework.type.AnnotatedTypeFactory; import org.checkerframework.framework.type.QualifierHierarchy; -import org.checkerframework.framework.type.poly.QualifierPolymorphism; import org.checkerframework.javacutil.AnnotationBuilder; import org.checkerframework.javacutil.AnnotationUtils; import org.checkerframework.javacutil.BugInCF; @@ -27,7 +28,8 @@ * *

        This class is immutable and can be only created through {@link MultiGraphFactory}. */ -public class MultiGraphQualifierHierarchy extends QualifierHierarchy { +@SuppressWarnings("interning") // TODO after https://tinyurl.com/cfissue/3404 is merged +public class MultiGraphQualifierHierarchy implements QualifierHierarchy { /** * Factory used to create an instance of {@link GraphQualifierHierarchy}. A factory can be used @@ -66,7 +68,7 @@ public static class MultiGraphFactory { *

          *
        • the argument to @PolymorphicQualifier (typically the top qualifier in the * hierarchy), or - *
        • "PolymorphicQualifier" if @PolymorphicQualifier is used without an argument, or + *
        • "Annotation" if @PolymorphicQualifier is used without an argument, or *
        */ protected final Map polyQualifiers; @@ -91,17 +93,73 @@ public void addQualifier(AnnotationMirror qual) { return; } - Name pqtopclass = QualifierPolymorphism.getPolymorphicQualifierElement(qual); + Name pqtopclass = getPolymorphicQualifierElement(qual); if (pqtopclass != null) { - AnnotationMirror pqtop = - AnnotationBuilder.fromName(atypeFactory.getElementUtils(), pqtopclass); - // use given top (which might be PolymorphicQualifier) as key + AnnotationMirror pqtop; + if (pqtopclass.contentEquals(Annotation.class.getName())) { + // A @PolymorphicQualifier with no value defaults to Annotation.class. + // That means there is only one top in the hierarchy. The top qualifier + // may not be known at this point, so use the qualifier itself. + // This is changed to top in MultiGraphQualifierHierarchy.addPolyRelations + pqtop = qual; + } else { + pqtop = AnnotationBuilder.fromName(atypeFactory.getElementUtils(), pqtopclass); + } + // use given top (which might be Annotation) as key this.polyQualifiers.put(pqtop, qual); } else { supertypesDirect.put(qual, AnnotationUtils.createAnnotationSet()); } } + /** + * Returns the {@link PolymorphicQualifier} meta-annotation on {@code qual} if one exists; + * otherwise return null. + * + * @param qual qualifier + * @return the {@link PolymorphicQualifier} meta-annotation on {@code qual} if one exists; + * otherwise return null + */ + private static AnnotationMirror getPolymorphicQualifier(AnnotationMirror qual) { + if (qual == null) { + return null; + } + Element qualElt = qual.getAnnotationType().asElement(); + for (AnnotationMirror am : qualElt.getAnnotationMirrors()) { + if (AnnotationUtils.areSameByClass(am, PolymorphicQualifier.class)) { + return am; + } + } + return null; + } + + /** + * If {@code qual} is a polymorphic qualifier, return the class specified by the {@link + * PolymorphicQualifier} meta-annotation on the polymorphic qualifier is returned. + * Otherwise, return null. + * + *

        This value identifies the qualifier hierarchy to which this polymorphic qualifier + * belongs. By convention, it is the top qualifier of the hierarchy. Use of {@code + * Annotation.class} is discouraged, because it can lead to ambiguity if used for multiple + * type systems. + * + * @param qual an annotation + * @return the class specified by the {@link PolymorphicQualifier} meta-annotation on {@code + * qual}, if {@code qual} is a polymorphic qualifier; otherwise, null. + * @see org.checkerframework.framework.qual.PolymorphicQualifier#value() + */ + private static Name getPolymorphicQualifierElement(AnnotationMirror qual) { + AnnotationMirror poly = getPolymorphicQualifier(qual); + + // System.out.println("poly: " + poly + " pq: " + + // PolymorphicQualifier.class.getCanonicalName()); + if (poly == null) { + return null; + } + Name ret = AnnotationUtils.getElementValueClassName(poly, "value", true); + return ret; + } + /** * Adds a subtype relationship between the two type qualifiers. Assumes that both qualifiers * are part of the same qualifier hierarchy; callers should ensure this. @@ -159,13 +217,6 @@ protected void assertNotBuilt() { /** The bottom qualifiers of the type hierarchies. TODO: clarify relation to tops. */ protected final Set bottoms; - /** - * Reference to the special qualifier org.checkerframework.framework.qual.PolymorphicQualifier. - * It is used as a key in polyQualifiers, if the qualifier hierarchy consists of a single top - * and no specific qualifier was specified. - */ - protected final AnnotationMirror polymorphicQualifier; - /** * See {@link MultiGraphQualifierHierarchy.MultiGraphFactory#polyQualifiers}. * @@ -195,9 +246,6 @@ public MultiGraphQualifierHierarchy(MultiGraphFactory f, Object... args) { Set newtops = findTops(supertypesTransitive); Set newbottoms = findBottoms(supertypesTransitive); - this.polymorphicQualifier = - AnnotationBuilder.fromClass( - f.atypeFactory.getElementUtils(), PolymorphicQualifier.class); this.polyQualifiers = f.polyQualifiers; addPolyRelations(this, supertypesTransitive, this.polyQualifiers, newtops, newbottoms); @@ -315,9 +363,7 @@ public AnnotationMirror getBottomAnnotation(AnnotationMirror start) { public AnnotationMirror getPolymorphicAnnotation(AnnotationMirror start) { AnnotationMirror top = getTopAnnotation(start); for (AnnotationMirror key : polyQualifiers.keySet()) { - if (key != null - && (AnnotationUtils.areSame(key, top) - || AnnotationUtils.areSame(key, polymorphicQualifier))) { + if (key != null && AnnotationUtils.areSame(key, top)) { return polyQualifiers.get(key); } } @@ -355,20 +401,6 @@ && isSubtype(rhsAnno, lhsAnno)) { return lhs.size() == valid; } - @Override - public boolean isSubtypeTypeVariable( - Collection subAnnos, - Collection superAnnos) { - for (AnnotationMirror top : getTopAnnotations()) { - AnnotationMirror rhsForTop = findAnnotationInHierarchy(subAnnos, top); - AnnotationMirror lhsForTop = findAnnotationInHierarchy(superAnnos, top); - if (!isSubtypeTypeVariable(rhsForTop, lhsForTop)) { - return false; - } - } - return true; - } - /** For caching results of lubs * */ private Map lubs = null; @@ -390,15 +422,6 @@ public AnnotationMirror leastUpperBound(AnnotationMirror a1, AnnotationMirror a2 return lubs.get(pair); } - @Override - public AnnotationMirror leastUpperBoundTypeVariable(AnnotationMirror a1, AnnotationMirror a2) { - if (a1 == null || a2 == null) { - // [] is a supertype of any qualifier, and [] <: [] - return null; - } - return leastUpperBound(a1, a2); - } - /** A cache of the results of glb computations. Maps from a pair of annotations to their glb. */ private Map glbs = null; @@ -414,20 +437,6 @@ public AnnotationMirror greatestLowerBound(AnnotationMirror a1, AnnotationMirror return glbs.get(pair); } - @Override - public AnnotationMirror greatestLowerBoundTypeVariable( - AnnotationMirror a1, AnnotationMirror a2) { - if (a1 == null) { - // [] is a supertype of any qualifier, and [] <: [] - return a2; - } - if (a2 == null) { - // [] is a supertype of any qualifier, and [] <: [] - return a1; - } - return greatestLowerBound(a1, a2); - } - /** * {@inheritDoc} * @@ -460,19 +469,12 @@ public boolean isSubtype(AnnotationMirror subAnno, AnnotationMirror superAnno) { return AnnotationUtils.containsSame(supermap1, superAnno); } - @Override - public boolean isSubtypeTypeVariable(AnnotationMirror subAnno, AnnotationMirror superAnno) { - if (superAnno == null) { - // [] is a supertype of any qualifier, and [] <: [] - return true; - } - if (subAnno == null) { - // [] is a subtype of no qualifier (only []) - return false; - } - return isSubtype(subAnno, superAnno); - } - + /** + * Throw a {@link BugInCF} if {@code a} is not in the {@link #supertypesTransitive} or {@link + * #polyQualifiers}. + * + * @param a qualifier + */ private final void checkAnnoInGraph(AnnotationMirror a) { if (AnnotationUtils.containsSame(supertypesTransitive.keySet(), a) || AnnotationUtils.containsSame(polyQualifiers.values(), a)) { @@ -564,77 +566,80 @@ protected void addPolyRelations( return; } + // Handle the case where @PolymorphicQualifier uses the default value Annotation.class. + if (polyQualifiers.size() == 1 && tops.size() == 1) { + Map.Entry entry = + polyQualifiers.entrySet().iterator().next(); + AnnotationMirror poly = entry.getKey(); + AnnotationMirror maybeTop = entry.getValue(); + if (AnnotationUtils.areSameByName(poly, maybeTop)) { + // If the value of @PolymorphicQualifier is the default value, Annotation.class, + // then map is set to polyQual -> polyQual in + // MultiGraphQualifierHierarchy.MultiGraphFactory.addQualifier, + // because the top is unknown there. + // Reset it to top here. + polyQualifiers.put(tops.iterator().next(), poly); + polyQualifiers.remove(poly); + } + } + for (Map.Entry kv : polyQualifiers.entrySet()) { AnnotationMirror declTop = kv.getKey(); AnnotationMirror polyQualifier = kv.getValue(); - if (AnnotationUtils.areSame(declTop, polymorphicQualifier)) { - if (tops.size() == 1) { // un-ambigous single top - AnnotationUtils.updateMappingToImmutableSet(fullMap, polyQualifier, tops); - for (AnnotationMirror bottom : bottoms) { - // Add the polyqualifier as a supertype - // Need to copy over the set as it is unmodifiable. - AnnotationUtils.updateMappingToImmutableSet( - fullMap, bottom, Collections.singleton(polyQualifier)); - } - } else { - throw new BugInCF( - "MultiGraphQualifierHierarchy.addPolyRelations: " - + "incorrect or missing top qualifier given in polymorphic qualifier " - + polyQualifier - + "; declTop = " - + declTop - + "; possible top qualifiers: " - + tops); - } + // Ensure that it's really the top of the hierarchy + Set declSupers = fullMap.get(declTop); + AnnotationMirror polyTop = null; + if (declSupers.isEmpty()) { + polyTop = declTop; } else { - // Ensure that it's really the top of the hierarchy - Set declSupers = fullMap.get(declTop); - AnnotationMirror polyTop = null; - if (declSupers.isEmpty()) { - polyTop = declTop; - } else { - for (AnnotationMirror ds : declSupers) { - if (AnnotationUtils.containsSameByName(tops, ds)) { - polyTop = ds; - } + for (AnnotationMirror ds : declSupers) { + if (AnnotationUtils.containsSameByName(tops, ds)) { + polyTop = ds; } } - boolean found = (polyTop != null); - if (found) { - AnnotationUtils.updateMappingToImmutableSet( - fullMap, polyQualifier, Collections.singleton(polyTop)); - } else { - throw new BugInCF( - "MultiGraphQualifierHierarchy.addPolyRelations: " - + "incorrect top qualifier given in polymorphic qualifier: " - + polyQualifier - + " could not find: " - + polyTop); - } + } + boolean found = (polyTop != null); + if (found) { + AnnotationUtils.updateMappingToImmutableSet( + fullMap, polyQualifier, Collections.singleton(polyTop)); + } else if (AnnotationUtils.areSameByName(polyQualifier, declTop)) { + throw new BugInCF( + "MultiGraphQualifierHierarchy.addPolyRelations: " + + "incorrect or missing top qualifier given in polymorphic qualifier " + + polyQualifier + + "; possible top qualifiers: " + + tops); + } else { + throw new BugInCF( + "MultiGraphQualifierHierarchy.addPolyRelations: " + + "incorrect top qualifier given in polymorphic qualifier: " + + polyQualifier + + " could not find: " + + polyTop); + } - found = false; - AnnotationMirror bottom = null; - outer: - for (AnnotationMirror btm : bottoms) { - for (AnnotationMirror btmsuper : fullMap.get(btm)) { - if (AnnotationUtils.areSameByName(btmsuper, polyTop)) { - found = true; - bottom = btm; - break outer; - } + found = false; + AnnotationMirror bottom = null; + outer: + for (AnnotationMirror btm : bottoms) { + for (AnnotationMirror btmsuper : fullMap.get(btm)) { + if (AnnotationUtils.areSameByName(btmsuper, polyTop)) { + found = true; + bottom = btm; + break outer; } } - if (found) { - AnnotationUtils.updateMappingToImmutableSet( - fullMap, bottom, Collections.singleton(polyQualifier)); - } else { - // TODO: in a type system with a single qualifier this check will fail. - // throw new BugInCF("MultiGraphQualifierHierarchy.addPolyRelations: - // " + - // "incorrect top qualifier given in polymorphic qualifier: " - // - // + polyQualifier + " could not find bottom for: " + polyTop); - } + } + if (found) { + AnnotationUtils.updateMappingToImmutableSet( + fullMap, bottom, Collections.singleton(polyQualifier)); + } else { + // TODO: in a type system with a single qualifier this check will fail. + // throw new BugInCF("MultiGraphQualifierHierarchy.addPolyRelations: + // " + + // "incorrect top qualifier given in polymorphic qualifier: " + // + // + polyQualifier + " could not find bottom for: " + polyTop); } } } @@ -723,8 +728,8 @@ private AnnotationMirror findLubWithPoly(AnnotationMirror poly, AnnotationMirror return getTopAnnotation(poly); } - /** Sees if a particular annotation mirror is a polymorphic qualifier. */ - private boolean isPolymorphicQualifier(AnnotationMirror qual) { + @Override + public boolean isPolymorphicQualifier(AnnotationMirror qual) { return AnnotationUtils.containsSame(polyQualifiers.values(), qual); } diff --git a/framework/src/main/java/org/checkerframework/framework/util/TreePathCacher.java b/framework/src/main/java/org/checkerframework/framework/util/TreePathCacher.java index d921b353b18..d2048847b34 100644 --- a/framework/src/main/java/org/checkerframework/framework/util/TreePathCacher.java +++ b/framework/src/main/java/org/checkerframework/framework/util/TreePathCacher.java @@ -6,6 +6,7 @@ import com.sun.source.util.TreeScanner; import java.util.HashMap; import java.util.Map; +import org.checkerframework.checker.interning.qual.FindDistinct; /** * TreePathCacher is a TreeScanner that creates and caches a TreePath for a target Tree. @@ -55,7 +56,7 @@ public void addPath(Tree target, TreePath path) { * @return the TreePath corresponding to target, or null if target is not found in the * compilation root */ - public TreePath getPath(CompilationUnitTree root, Tree target) { + public TreePath getPath(CompilationUnitTree root, @FindDistinct Tree target) { if (foundPaths.containsKey(target)) { return foundPaths.get(target); } @@ -89,6 +90,7 @@ public void clear() { } /** Scan a single node. The current path is updated for the duration of the scan. */ + @SuppressWarnings("interning:not.interned") // assertion @Override public TreePath scan(Tree tree, Tree target) { TreePath prev = path; diff --git a/framework/src/main/java/org/checkerframework/framework/util/defaults/QualifierDefaults.java b/framework/src/main/java/org/checkerframework/framework/util/defaults/QualifierDefaults.java index 955a9d21ef5..828143c96f9 100644 --- a/framework/src/main/java/org/checkerframework/framework/util/defaults/QualifierDefaults.java +++ b/framework/src/main/java/org/checkerframework/framework/util/defaults/QualifierDefaults.java @@ -25,6 +25,7 @@ import javax.lang.model.element.TypeParameterElement; import javax.lang.model.type.TypeKind; import javax.lang.model.util.Elements; +import org.checkerframework.checker.interning.qual.FindDistinct; import org.checkerframework.framework.qual.AnnotatedFor; import org.checkerframework.framework.qual.DefaultQualifier; import org.checkerframework.framework.qual.TypeUseLocation; @@ -400,7 +401,9 @@ private Element nearestEnclosingExceptLocal(Tree tree) { case VARIABLE: VariableTree vtree = (VariableTree) t; ExpressionTree vtreeInit = vtree.getInitializer(); - if (vtreeInit != null && prev == vtreeInit) { + @SuppressWarnings("interning:not.interned") // check cached value + boolean sameAsPrev = (vtreeInit != null && prev == vtreeInit); + if (sameAsPrev) { Element elt = TreeUtils.elementFromDeclaration((VariableTree) t); AnnotationMirror d = atypeFactory.getDeclAnnotation(elt, DefaultQualifier.class); @@ -790,7 +793,7 @@ protected boolean shouldBeAnnotated( return !(type == null // TODO: executables themselves should not be annotated - // For some reason testchecker-tests fails with this. + // For some reason h1h2checker-tests fails with this. // || type.getKind() == TypeKind.EXECUTABLE || type.getKind() == TypeKind.NONE || type.getKind() == TypeKind.WILDCARD @@ -832,7 +835,7 @@ protected class DefaultApplierElementImpl extends AnnotatedTypeScanner { @Override - public Void scan(AnnotatedTypeMirror t, AnnotationMirror qual) { + public Void scan(@FindDistinct AnnotatedTypeMirror t, AnnotationMirror qual) { if (!shouldBeAnnotated(t, t == defaultableTypeVar)) { return super.scan(t, qual); } diff --git a/framework/src/main/java/org/checkerframework/framework/util/element/ElementAnnotationUtil.java b/framework/src/main/java/org/checkerframework/framework/util/element/ElementAnnotationUtil.java index 9c3234cbbe7..fac6a4f87b7 100644 --- a/framework/src/main/java/org/checkerframework/framework/util/element/ElementAnnotationUtil.java +++ b/framework/src/main/java/org/checkerframework/framework/util/element/ElementAnnotationUtil.java @@ -84,6 +84,7 @@ public static void applyAllElementAnnotations( * @param type the type to annotate * @param annotations the annotations to add */ + @SuppressWarnings("interning:not.interned") // AST node comparison static void addDeclarationAnnotationsFromElement( final AnnotatedTypeMirror type, final List annotations) { // The code here should be similar to diff --git a/framework/src/main/java/org/checkerframework/framework/util/typeinference/TypeArgInferenceUtil.java b/framework/src/main/java/org/checkerframework/framework/util/typeinference/TypeArgInferenceUtil.java index 8287a330dc2..1e2c322ffa5 100644 --- a/framework/src/main/java/org/checkerframework/framework/util/typeinference/TypeArgInferenceUtil.java +++ b/framework/src/main/java/org/checkerframework/framework/util/typeinference/TypeArgInferenceUtil.java @@ -42,6 +42,7 @@ import javax.lang.model.type.TypeVisitor; import javax.lang.model.type.UnionType; import javax.lang.model.util.Types; +import org.checkerframework.checker.interning.qual.FindDistinct; import org.checkerframework.framework.type.AnnotatedTypeFactory; import org.checkerframework.framework.type.AnnotatedTypeMirror; import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedArrayType; @@ -136,8 +137,11 @@ public static Set methodTypeToTargets(final AnnotatedExecutableTyp * assignment context. Returns the annotated type that the method invocation at the leaf is * assigned to. If the result is a primitive, return the boxed version. * - * @return type that path leaf is assigned to + * @param atypeFactory the type factory, for looking up types + * @param path the path whole leaf to look up a type for + * @return the type of path's leaf */ + @SuppressWarnings("interning:not.interned") // AST node comparisons public static AnnotatedTypeMirror assignedTo(AnnotatedTypeFactory atypeFactory, TreePath path) { Tree assignmentContext = TreeUtils.getAssignmentContext(path); AnnotatedTypeMirror res; @@ -264,10 +268,14 @@ private static AnnotatedTypeMirror assignedToExecutable( } /** - * Returns whether argumentTree is the tree at the leaf of path. if tree is a conditional + * Returns whether argumentTree is the tree at the leaf of path. If tree is a conditional * expression, isArgument is called recursively on the true and false expressions. + * + * @param path the path whose leaf to test + * @param argumentTree the expression that might be path's leaf + * @return true if {@code argumentTree} is the leaf of {@code path} */ - private static boolean isArgument(TreePath path, ExpressionTree argumentTree) { + private static boolean isArgument(TreePath path, @FindDistinct ExpressionTree argumentTree) { argumentTree = TreeUtils.withoutParens(argumentTree); if (argumentTree == path.getLeaf()) { return true; diff --git a/framework/src/main/java/org/checkerframework/framework/util/typeinference/TypeArgumentInference.java b/framework/src/main/java/org/checkerframework/framework/util/typeinference/TypeArgumentInference.java index 89d42ba4a22..b0249a9a1dd 100644 --- a/framework/src/main/java/org/checkerframework/framework/util/typeinference/TypeArgumentInference.java +++ b/framework/src/main/java/org/checkerframework/framework/util/typeinference/TypeArgumentInference.java @@ -29,8 +29,8 @@ * *

        For the Checker Framework we also need to infer reasonable annotations for these type * arguments. For information on inferring type arguments see the documentation in JLS7 and JLS8: - * https://docs.oracle.com/javase/specs/jls/se11/html/jls-18.html - * https://docs.oracle.com/javase/specs/jls/se11/html/jls-15.html#jls-15.12.2.7 + * https://docs.oracle.com/javase/specs/jls/se8/html/jls-18.html + * https://docs.oracle.com/javase/specs/jls/se7/html/jls-15.html#jls-15.12.2.7 * *

        Note: It appears that Java 8 greatly improved the type argument inference and related error * messaging but I have found it useful to consult the JLS 7 as well. diff --git a/framework/src/main/java/org/checkerframework/framework/util/typeinference/constraint/AFConstraint.java b/framework/src/main/java/org/checkerframework/framework/util/typeinference/constraint/AFConstraint.java index 90cd2c8a7b1..64d52602c93 100644 --- a/framework/src/main/java/org/checkerframework/framework/util/typeinference/constraint/AFConstraint.java +++ b/framework/src/main/java/org/checkerframework/framework/util/typeinference/constraint/AFConstraint.java @@ -13,8 +13,9 @@ * invocations and new class invocations. These constraints are simplified then converted to * TUConstraints during type argument inference. * - *

        Subclasses of AFConstraint represent the following types of constraints found in - * (https://docs.oracle.com/javase/specs/jls/se11/html/jls-15.html#jls-15.12.2.7) + *

        Subclasses of AFConstraint represent the following types of + * constraints: * *

        A 《 F and F 》 A both imply that A is convertible to F. F 《 A and A 》 F both imply that F is * convertible to A (this may happen due to wildcard/typevar bounds and recursive types) A = F diff --git a/framework/src/main/java/org/checkerframework/framework/util/typeinference/solver/EqualitiesSolver.java b/framework/src/main/java/org/checkerframework/framework/util/typeinference/solver/EqualitiesSolver.java index 472301bfd60..8c2e20dd26a 100644 --- a/framework/src/main/java/org/checkerframework/framework/util/typeinference/solver/EqualitiesSolver.java +++ b/framework/src/main/java/org/checkerframework/framework/util/typeinference/solver/EqualitiesSolver.java @@ -7,6 +7,7 @@ import javax.lang.model.element.AnnotationMirror; import javax.lang.model.type.TypeKind; import javax.lang.model.type.TypeVariable; +import org.checkerframework.checker.interning.qual.FindDistinct; import org.checkerframework.framework.type.AnnotatedTypeFactory; import org.checkerframework.framework.type.AnnotatedTypeMirror; import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedTypeVariable; @@ -83,7 +84,7 @@ public InferenceResult solveEqualities( /** * Let Ti be a target type parameter. When we reach this method we have inferred an argument, - * Ai, for Ti + * Ai, for Ti. * *

        However, there still may be constraints of the form {@literal Ti = Tj}, {@literal Ti <: * Tj}, {@literal Tj <: Ti} in the constraint map. In this case we need to replace Ti with the @@ -95,9 +96,10 @@ public InferenceResult solveEqualities( * * @param target the target for which we have inferred a concrete type argument * @param type the type inferred + * @param constraints the constraints that are side-effected by this method */ private void rewriteWithInferredType( - final TypeVariable target, + final @FindDistinct TypeVariable target, final AnnotatedTypeMirror type, final ConstraintMap constraints) { @@ -184,10 +186,12 @@ private void rewriteWithInferredType( * * @param target the target for which we know another target is exactly equal to this target * @param inferredTarget the other target inferred to be equal + * @param constraints the constraints that are side-effected by this method + * @param typeFactory type factory */ private void rewriteWithInferredTarget( - final TypeVariable target, - final TypeVariable inferredTarget, + final @FindDistinct TypeVariable target, + final @FindDistinct TypeVariable inferredTarget, final ConstraintMap constraints, final AnnotatedTypeFactory typeFactory) { final TargetConstraints targetRecord = constraints.getConstraints(target); diff --git a/framework/src/test/java/testchecker/TestAnnotatedTypeFactory.java b/framework/src/test/java/h1h2checker/H1H2AnnotatedTypeFactory.java similarity index 76% rename from framework/src/test/java/testchecker/TestAnnotatedTypeFactory.java rename to framework/src/test/java/h1h2checker/H1H2AnnotatedTypeFactory.java index ca3aba582ba..37e882d094a 100644 --- a/framework/src/test/java/testchecker/TestAnnotatedTypeFactory.java +++ b/framework/src/test/java/h1h2checker/H1H2AnnotatedTypeFactory.java @@ -1,8 +1,19 @@ -package testchecker; +package h1h2checker; import com.sun.source.tree.Tree; import com.sun.source.tree.Tree.Kind; import com.sun.source.tree.VariableTree; +import h1h2checker.quals.H1Bot; +import h1h2checker.quals.H1Invalid; +import h1h2checker.quals.H1Poly; +import h1h2checker.quals.H1S1; +import h1h2checker.quals.H1S2; +import h1h2checker.quals.H1Top; +import h1h2checker.quals.H2Bot; +import h1h2checker.quals.H2Poly; +import h1h2checker.quals.H2S1; +import h1h2checker.quals.H2S2; +import h1h2checker.quals.H2Top; import java.lang.annotation.Annotation; import java.util.Set; import javax.lang.model.element.AnnotationMirror; @@ -13,26 +24,15 @@ import org.checkerframework.framework.util.MultiGraphQualifierHierarchy; import org.checkerframework.framework.util.MultiGraphQualifierHierarchy.MultiGraphFactory; import org.checkerframework.javacutil.AnnotationBuilder; -import testchecker.quals.H1Bot; -import testchecker.quals.H1Invalid; -import testchecker.quals.H1Poly; -import testchecker.quals.H1S1; -import testchecker.quals.H1S2; -import testchecker.quals.H1Top; -import testchecker.quals.H2Bot; -import testchecker.quals.H2Poly; -import testchecker.quals.H2S1; -import testchecker.quals.H2S2; -import testchecker.quals.H2Top; -public class TestAnnotatedTypeFactory extends BaseAnnotatedTypeFactory { +public class H1H2AnnotatedTypeFactory extends BaseAnnotatedTypeFactory { AnnotationMirror H1S2; - public TestAnnotatedTypeFactory(BaseTypeChecker checker) { + public H1H2AnnotatedTypeFactory(BaseTypeChecker checker) { super(checker); this.postInit(); - H1S2 = AnnotationBuilder.fromClass(elements, testchecker.quals.H1S2.class); + H1S2 = AnnotationBuilder.fromClass(elements, h1h2checker.quals.H1S2.class); } @Override diff --git a/framework/src/test/java/h1h2checker/H1H2Checker.java b/framework/src/test/java/h1h2checker/H1H2Checker.java new file mode 100644 index 00000000000..5f1f104ab7d --- /dev/null +++ b/framework/src/test/java/h1h2checker/H1H2Checker.java @@ -0,0 +1,5 @@ +package h1h2checker; + +import org.checkerframework.common.basetype.BaseTypeChecker; + +public class H1H2Checker extends BaseTypeChecker {} diff --git a/framework/src/test/java/testchecker/TestVisitor.java b/framework/src/test/java/h1h2checker/H1H2Visitor.java similarity index 79% rename from framework/src/test/java/testchecker/TestVisitor.java rename to framework/src/test/java/h1h2checker/H1H2Visitor.java index fbcd8aa4bc1..d26977ed398 100644 --- a/framework/src/test/java/testchecker/TestVisitor.java +++ b/framework/src/test/java/h1h2checker/H1H2Visitor.java @@ -1,6 +1,7 @@ -package testchecker; +package h1h2checker; import com.sun.source.tree.Tree; +import h1h2checker.quals.H1Invalid; import javax.lang.model.element.AnnotationMirror; import org.checkerframework.common.basetype.BaseTypeChecker; import org.checkerframework.common.basetype.BaseTypeValidator; @@ -9,22 +10,21 @@ import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedDeclaredType; import org.checkerframework.framework.util.AnnotatedTypes; import org.checkerframework.javacutil.AnnotationBuilder; -import testchecker.quals.H1Invalid; -public class TestVisitor extends BaseTypeVisitor { +public class H1H2Visitor extends BaseTypeVisitor { - public TestVisitor(BaseTypeChecker checker) { + public H1H2Visitor(BaseTypeChecker checker) { super(checker); } @Override protected BaseTypeValidator createTypeValidator() { - return new TestTypeValidator(checker, this, atypeFactory); + return new H1H2TypeValidator(checker, this, atypeFactory); } - private final class TestTypeValidator extends BaseTypeValidator { + private final class H1H2TypeValidator extends BaseTypeValidator { - public TestTypeValidator( + public H1H2TypeValidator( BaseTypeChecker checker, BaseTypeVisitor visitor, AnnotatedTypeFactory atypeFactory) { @@ -39,7 +39,7 @@ public Void visitDeclared(AnnotatedDeclaredType type, Tree p) { p, // An error specific to this type system, with no corresponding text // in a messages.properties file; this checker is just for testing. - "testchecker.h1invalid.forbidden", + "h1h2checker.h1invalid.forbidden", type.getAnnotations(), type.toString()); } diff --git a/framework/src/test/java/testchecker/quals/H1Bot.java b/framework/src/test/java/h1h2checker/quals/H1Bot.java similarity index 95% rename from framework/src/test/java/testchecker/quals/H1Bot.java rename to framework/src/test/java/h1h2checker/quals/H1Bot.java index 2a1ec4f6b34..e9ea140935d 100644 --- a/framework/src/test/java/testchecker/quals/H1Bot.java +++ b/framework/src/test/java/h1h2checker/quals/H1Bot.java @@ -1,4 +1,4 @@ -package testchecker.quals; +package h1h2checker.quals; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; diff --git a/framework/src/test/java/testchecker/quals/H1Invalid.java b/framework/src/test/java/h1h2checker/quals/H1Invalid.java similarity index 93% rename from framework/src/test/java/testchecker/quals/H1Invalid.java rename to framework/src/test/java/h1h2checker/quals/H1Invalid.java index 91d275f3d18..48fbb60870c 100644 --- a/framework/src/test/java/testchecker/quals/H1Invalid.java +++ b/framework/src/test/java/h1h2checker/quals/H1Invalid.java @@ -1,4 +1,4 @@ -package testchecker.quals; +package h1h2checker.quals; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; diff --git a/framework/src/test/java/testchecker/quals/H1Poly.java b/framework/src/test/java/h1h2checker/quals/H1Poly.java similarity index 94% rename from framework/src/test/java/testchecker/quals/H1Poly.java rename to framework/src/test/java/h1h2checker/quals/H1Poly.java index 7951e9e0203..840fb0cf82b 100644 --- a/framework/src/test/java/testchecker/quals/H1Poly.java +++ b/framework/src/test/java/h1h2checker/quals/H1Poly.java @@ -1,4 +1,4 @@ -package testchecker.quals; +package h1h2checker.quals; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; diff --git a/framework/src/test/java/testchecker/quals/H1S1.java b/framework/src/test/java/h1h2checker/quals/H1S1.java similarity index 93% rename from framework/src/test/java/testchecker/quals/H1S1.java rename to framework/src/test/java/h1h2checker/quals/H1S1.java index b8dfabc51f5..10741dc05c7 100644 --- a/framework/src/test/java/testchecker/quals/H1S1.java +++ b/framework/src/test/java/h1h2checker/quals/H1S1.java @@ -1,4 +1,4 @@ -package testchecker.quals; +package h1h2checker.quals; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; diff --git a/framework/src/test/java/testchecker/quals/H1S2.java b/framework/src/test/java/h1h2checker/quals/H1S2.java similarity index 93% rename from framework/src/test/java/testchecker/quals/H1S2.java rename to framework/src/test/java/h1h2checker/quals/H1S2.java index e9b163465f8..f9a8d1edff9 100644 --- a/framework/src/test/java/testchecker/quals/H1S2.java +++ b/framework/src/test/java/h1h2checker/quals/H1S2.java @@ -1,4 +1,4 @@ -package testchecker.quals; +package h1h2checker.quals; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; diff --git a/framework/src/test/java/testchecker/quals/H1Top.java b/framework/src/test/java/h1h2checker/quals/H1Top.java similarity index 94% rename from framework/src/test/java/testchecker/quals/H1Top.java rename to framework/src/test/java/h1h2checker/quals/H1Top.java index 423ab581be5..cb3f0dc5439 100644 --- a/framework/src/test/java/testchecker/quals/H1Top.java +++ b/framework/src/test/java/h1h2checker/quals/H1Top.java @@ -1,4 +1,4 @@ -package testchecker.quals; +package h1h2checker.quals; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; diff --git a/framework/src/test/java/testchecker/quals/H2Bot.java b/framework/src/test/java/h1h2checker/quals/H2Bot.java similarity index 95% rename from framework/src/test/java/testchecker/quals/H2Bot.java rename to framework/src/test/java/h1h2checker/quals/H2Bot.java index fb39b6fb8f8..2564b7c36a7 100644 --- a/framework/src/test/java/testchecker/quals/H2Bot.java +++ b/framework/src/test/java/h1h2checker/quals/H2Bot.java @@ -1,4 +1,4 @@ -package testchecker.quals; +package h1h2checker.quals; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; diff --git a/framework/src/test/java/testchecker/quals/H2Poly.java b/framework/src/test/java/h1h2checker/quals/H2Poly.java similarity index 94% rename from framework/src/test/java/testchecker/quals/H2Poly.java rename to framework/src/test/java/h1h2checker/quals/H2Poly.java index 52b4a497018..326c78c2096 100644 --- a/framework/src/test/java/testchecker/quals/H2Poly.java +++ b/framework/src/test/java/h1h2checker/quals/H2Poly.java @@ -1,4 +1,4 @@ -package testchecker.quals; +package h1h2checker.quals; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; diff --git a/framework/src/test/java/testchecker/quals/H2S1.java b/framework/src/test/java/h1h2checker/quals/H2S1.java similarity index 93% rename from framework/src/test/java/testchecker/quals/H2S1.java rename to framework/src/test/java/h1h2checker/quals/H2S1.java index e86352b8948..3147f9fb212 100644 --- a/framework/src/test/java/testchecker/quals/H2S1.java +++ b/framework/src/test/java/h1h2checker/quals/H2S1.java @@ -1,4 +1,4 @@ -package testchecker.quals; +package h1h2checker.quals; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; diff --git a/framework/src/test/java/testchecker/quals/H2S2.java b/framework/src/test/java/h1h2checker/quals/H2S2.java similarity index 93% rename from framework/src/test/java/testchecker/quals/H2S2.java rename to framework/src/test/java/h1h2checker/quals/H2S2.java index 354552b181e..916c4f93156 100644 --- a/framework/src/test/java/testchecker/quals/H2S2.java +++ b/framework/src/test/java/h1h2checker/quals/H2S2.java @@ -1,4 +1,4 @@ -package testchecker.quals; +package h1h2checker.quals; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; diff --git a/framework/src/test/java/testchecker/quals/H2Top.java b/framework/src/test/java/h1h2checker/quals/H2Top.java similarity index 94% rename from framework/src/test/java/testchecker/quals/H2Top.java rename to framework/src/test/java/h1h2checker/quals/H2Top.java index af404205eb7..4488d628740 100644 --- a/framework/src/test/java/testchecker/quals/H2Top.java +++ b/framework/src/test/java/h1h2checker/quals/H2Top.java @@ -1,4 +1,4 @@ -package testchecker.quals; +package h1h2checker.quals; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; diff --git a/framework/src/test/java/testaccumulation/TestAccumulationAnnotatedTypeFactory.java b/framework/src/test/java/testaccumulation/TestAccumulationAnnotatedTypeFactory.java index 76fc374c37d..f6e67f2f175 100644 --- a/framework/src/test/java/testaccumulation/TestAccumulationAnnotatedTypeFactory.java +++ b/framework/src/test/java/testaccumulation/TestAccumulationAnnotatedTypeFactory.java @@ -10,6 +10,7 @@ import org.checkerframework.javacutil.TreeUtils; import testaccumulation.qual.TestAccumulation; import testaccumulation.qual.TestAccumulationBottom; +import testaccumulation.qual.TestAccumulationPredicate; /** * The annotated type factory for a test accumulation checker, which implements a basic called @@ -22,7 +23,11 @@ public class TestAccumulationAnnotatedTypeFactory extends AccumulationAnnotatedT * @param checker the checker */ public TestAccumulationAnnotatedTypeFactory(BaseTypeChecker checker) { - super(checker, TestAccumulation.class, TestAccumulationBottom.class); + super( + checker, + TestAccumulation.class, + TestAccumulationBottom.class, + TestAccumulationPredicate.class); this.postInit(); } diff --git a/framework/src/test/java/testaccumulation/qual/TestAccumulationBottom.java b/framework/src/test/java/testaccumulation/qual/TestAccumulationBottom.java index 46eadffed13..f7803610432 100644 --- a/framework/src/test/java/testaccumulation/qual/TestAccumulationBottom.java +++ b/framework/src/test/java/testaccumulation/qual/TestAccumulationBottom.java @@ -9,7 +9,7 @@ import org.checkerframework.framework.qual.TypeUseLocation; /** A test bottom type for an accumulation type system. */ -@SubtypeOf({TestAccumulation.class}) +@SubtypeOf({TestAccumulation.class, TestAccumulationPredicate.class}) @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) @TargetLocations({TypeUseLocation.EXPLICIT_LOWER_BOUND, TypeUseLocation.EXPLICIT_UPPER_BOUND}) diff --git a/framework/src/test/java/testaccumulation/qual/TestAccumulationPredicate.java b/framework/src/test/java/testaccumulation/qual/TestAccumulationPredicate.java new file mode 100644 index 00000000000..4afc02f8cda --- /dev/null +++ b/framework/src/test/java/testaccumulation/qual/TestAccumulationPredicate.java @@ -0,0 +1,15 @@ +package testaccumulation.qual; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import org.checkerframework.framework.qual.SubtypeOf; + +/** A test accumulation predicate annotation. */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) +@SubtypeOf({TestAccumulation.class}) +public @interface TestAccumulationPredicate { + String value(); +} diff --git a/framework/src/test/java/testchecker/TestChecker.java b/framework/src/test/java/testchecker/TestChecker.java deleted file mode 100644 index 8550e51f30c..00000000000 --- a/framework/src/test/java/testchecker/TestChecker.java +++ /dev/null @@ -1,5 +0,0 @@ -package testchecker; - -import org.checkerframework.common.basetype.BaseTypeChecker; - -public class TestChecker extends BaseTypeChecker {} diff --git a/framework/src/test/java/testlib/util/TestChecker.java b/framework/src/test/java/testlib/util/EvenOddChecker.java similarity index 98% rename from framework/src/test/java/testlib/util/TestChecker.java rename to framework/src/test/java/testlib/util/EvenOddChecker.java index 06bb4025a0c..20acc5e4b87 100644 --- a/framework/src/test/java/testlib/util/TestChecker.java +++ b/framework/src/test/java/testlib/util/EvenOddChecker.java @@ -26,7 +26,7 @@ * *

        This checker should only be used for testing the framework. */ -public final class TestChecker extends BaseTypeChecker { +public final class EvenOddChecker extends BaseTypeChecker { @Override protected BaseTypeVisitor createSourceVisitor() { return new TestVisitor(this); diff --git a/framework/src/test/java/tests/AccumulationTest.java b/framework/src/test/java/tests/AccumulationTest.java index 40d01c42665..8f9143fb9c1 100644 --- a/framework/src/test/java/tests/AccumulationTest.java +++ b/framework/src/test/java/tests/AccumulationTest.java @@ -2,7 +2,7 @@ import java.io.File; import java.util.List; -import org.checkerframework.framework.test.FrameworkPerDirectoryTest; +import org.checkerframework.framework.test.CheckerFrameworkPerDirectoryTest; import org.junit.runners.Parameterized.Parameters; import testaccumulation.TestAccumulationChecker; @@ -10,11 +10,17 @@ * A test that the accumulation abstract checker is working correctly, using a simple accumulation * checker. */ -public class AccumulationTest extends FrameworkPerDirectoryTest { +public class AccumulationTest extends CheckerFrameworkPerDirectoryTest { /** @param testFiles the files containing test code, which will be type-checked */ public AccumulationTest(List testFiles) { - super(testFiles, TestAccumulationChecker.class, "accumulation", "-Anomsgtext"); + super( + testFiles, + TestAccumulationChecker.class, + "accumulation", + "-Anomsgtext", + "-encoding", + "UTF-8"); } @Parameters diff --git a/framework/src/test/java/tests/AggregateTest.java b/framework/src/test/java/tests/AggregateTest.java index 2d8181188c0..bb575cd0444 100644 --- a/framework/src/test/java/tests/AggregateTest.java +++ b/framework/src/test/java/tests/AggregateTest.java @@ -2,11 +2,11 @@ import java.io.File; import java.util.List; -import org.checkerframework.framework.test.FrameworkPerDirectoryTest; +import org.checkerframework.framework.test.CheckerFrameworkPerDirectoryTest; import org.junit.runners.Parameterized.Parameters; import testlib.aggregate.AggregateOfCompoundChecker; -public class AggregateTest extends FrameworkPerDirectoryTest { +public class AggregateTest extends CheckerFrameworkPerDirectoryTest { /** @param testFiles the files containing test code, which will be type-checked */ public AggregateTest(List testFiles) { diff --git a/framework/src/test/java/tests/AliasingTest.java b/framework/src/test/java/tests/AliasingTest.java index 96783ceca39..5245cb12c01 100644 --- a/framework/src/test/java/tests/AliasingTest.java +++ b/framework/src/test/java/tests/AliasingTest.java @@ -2,10 +2,10 @@ import java.io.File; import java.util.List; -import org.checkerframework.framework.test.FrameworkPerDirectoryTest; +import org.checkerframework.framework.test.CheckerFrameworkPerDirectoryTest; import org.junit.runners.Parameterized.Parameters; -public class AliasingTest extends FrameworkPerDirectoryTest { +public class AliasingTest extends CheckerFrameworkPerDirectoryTest { /** @param testFiles the files containing test code, which will be type-checked */ public AliasingTest(List testFiles) { @@ -13,8 +13,7 @@ public AliasingTest(List testFiles) { testFiles, org.checkerframework.common.aliasing.AliasingChecker.class, "aliasing", - "-Anomsgtext", - "-Astubs=tests/aliasing/stubfile.astub"); + "-Anomsgtext"); } @Parameters diff --git a/framework/src/test/java/tests/AnnotatedForTest.java b/framework/src/test/java/tests/AnnotatedForTest.java index 3208d5f43ef..aa27ee2093d 100644 --- a/framework/src/test/java/tests/AnnotatedForTest.java +++ b/framework/src/test/java/tests/AnnotatedForTest.java @@ -2,11 +2,11 @@ import java.io.File; import java.util.List; -import org.checkerframework.framework.test.FrameworkPerDirectoryTest; +import org.checkerframework.framework.test.CheckerFrameworkPerDirectoryTest; import org.junit.runners.Parameterized.Parameters; /** Created by jthaine on 6/25/15. */ -public class AnnotatedForTest extends FrameworkPerDirectoryTest { +public class AnnotatedForTest extends CheckerFrameworkPerDirectoryTest { /** @param testFiles the files containing test code, which will be type-checked */ public AnnotatedForTest(List testFiles) { diff --git a/framework/src/test/java/tests/ClassValTest.java b/framework/src/test/java/tests/ClassValTest.java index 018fe883411..b2c4d43a714 100644 --- a/framework/src/test/java/tests/ClassValTest.java +++ b/framework/src/test/java/tests/ClassValTest.java @@ -2,11 +2,11 @@ import java.io.File; import java.util.List; -import org.checkerframework.framework.test.FrameworkPerDirectoryTest; +import org.checkerframework.framework.test.CheckerFrameworkPerDirectoryTest; import org.junit.runners.Parameterized.Parameters; /** Tests the ClassVal Checker. */ -public class ClassValTest extends FrameworkPerDirectoryTest { +public class ClassValTest extends CheckerFrameworkPerDirectoryTest { /** @param testFiles the files containing test code, which will be type-checked */ public ClassValTest(List testFiles) { diff --git a/framework/src/test/java/tests/CompoundCheckerTest.java b/framework/src/test/java/tests/CompoundCheckerTest.java index 190c060cb51..35a1a6970cc 100644 --- a/framework/src/test/java/tests/CompoundCheckerTest.java +++ b/framework/src/test/java/tests/CompoundCheckerTest.java @@ -2,12 +2,12 @@ import java.io.File; import java.util.List; -import org.checkerframework.framework.test.FrameworkPerDirectoryTest; +import org.checkerframework.framework.test.CheckerFrameworkPerDirectoryTest; import org.junit.runners.Parameterized.Parameters; import testlib.compound.CompoundChecker; /** Tests for the compound checker design pattern. */ -public class CompoundCheckerTest extends FrameworkPerDirectoryTest { +public class CompoundCheckerTest extends CheckerFrameworkPerDirectoryTest { /** @param testFiles the files containing test code, which will be type-checked */ public CompoundCheckerTest(List testFiles) { diff --git a/framework/src/test/java/tests/DefaultingLowerBoundTest.java b/framework/src/test/java/tests/DefaultingLowerBoundTest.java index 86acb5ad84c..af519b1d69e 100644 --- a/framework/src/test/java/tests/DefaultingLowerBoundTest.java +++ b/framework/src/test/java/tests/DefaultingLowerBoundTest.java @@ -2,12 +2,12 @@ import java.io.File; import java.util.List; -import org.checkerframework.framework.test.FrameworkPerDirectoryTest; +import org.checkerframework.framework.test.CheckerFrameworkPerDirectoryTest; import org.junit.runners.Parameterized.Parameters; import testlib.defaulting.DefaultingLowerBoundChecker; /** Created by jburke on 9/29/14. */ -public class DefaultingLowerBoundTest extends FrameworkPerDirectoryTest { +public class DefaultingLowerBoundTest extends CheckerFrameworkPerDirectoryTest { /** @param testFiles the files containing test code, which will be type-checked */ public DefaultingLowerBoundTest(List testFiles) { diff --git a/framework/src/test/java/tests/DefaultingUpperBoundTest.java b/framework/src/test/java/tests/DefaultingUpperBoundTest.java index e46a4ddfc46..d28ed11db34 100644 --- a/framework/src/test/java/tests/DefaultingUpperBoundTest.java +++ b/framework/src/test/java/tests/DefaultingUpperBoundTest.java @@ -2,12 +2,12 @@ import java.io.File; import java.util.List; -import org.checkerframework.framework.test.FrameworkPerDirectoryTest; +import org.checkerframework.framework.test.CheckerFrameworkPerDirectoryTest; import org.junit.runners.Parameterized.Parameters; import testlib.defaulting.DefaultingUpperBoundChecker; /** Created by jburke on 9/29/14. */ -public class DefaultingUpperBoundTest extends FrameworkPerDirectoryTest { +public class DefaultingUpperBoundTest extends CheckerFrameworkPerDirectoryTest { /** @param testFiles the files containing test code, which will be type-checked */ public DefaultingUpperBoundTest(List testFiles) { diff --git a/framework/src/test/java/tests/Flow2Test.java b/framework/src/test/java/tests/Flow2Test.java index 42c81a5d490..28ccbd4f554 100644 --- a/framework/src/test/java/tests/Flow2Test.java +++ b/framework/src/test/java/tests/Flow2Test.java @@ -2,7 +2,7 @@ import java.io.File; import java.util.List; -import org.checkerframework.framework.test.FrameworkPerDirectoryTest; +import org.checkerframework.framework.test.CheckerFrameworkPerDirectoryTest; import org.junit.runners.Parameterized.Parameters; import testlib.util.FlowTestChecker; @@ -11,7 +11,7 @@ * FlowTest} and have been written when the org.checkerframework.dataflow analysis has been * completely rewritten. */ -public class Flow2Test extends FrameworkPerDirectoryTest { +public class Flow2Test extends CheckerFrameworkPerDirectoryTest { /** @param testFiles the files containing test code, which will be type-checked */ public Flow2Test(List testFiles) { diff --git a/framework/src/test/java/tests/FlowExpressionCheckerTest.java b/framework/src/test/java/tests/FlowExpressionCheckerTest.java index 978325071af..a04808e7c3d 100644 --- a/framework/src/test/java/tests/FlowExpressionCheckerTest.java +++ b/framework/src/test/java/tests/FlowExpressionCheckerTest.java @@ -2,11 +2,11 @@ import java.io.File; import java.util.List; -import org.checkerframework.framework.test.FrameworkPerDirectoryTest; +import org.checkerframework.framework.test.CheckerFrameworkPerDirectoryTest; import org.junit.runners.Parameterized.Parameters; import testlib.flowexpression.FlowExpressionChecker; -public class FlowExpressionCheckerTest extends FrameworkPerDirectoryTest { +public class FlowExpressionCheckerTest extends CheckerFrameworkPerDirectoryTest { /** @param testFiles the files containing test code, which will be type-checked */ public FlowExpressionCheckerTest(List testFiles) { diff --git a/framework/src/test/java/tests/FlowTest.java b/framework/src/test/java/tests/FlowTest.java index 4d527c3ebcf..04826808ed4 100644 --- a/framework/src/test/java/tests/FlowTest.java +++ b/framework/src/test/java/tests/FlowTest.java @@ -2,12 +2,12 @@ import java.io.File; import java.util.List; -import org.checkerframework.framework.test.FrameworkPerDirectoryTest; +import org.checkerframework.framework.test.CheckerFrameworkPerDirectoryTest; import org.junit.runners.Parameterized.Parameters; import testlib.util.FlowTestChecker; /** */ -public class FlowTest extends FrameworkPerDirectoryTest { +public class FlowTest extends CheckerFrameworkPerDirectoryTest { /** @param testFiles the files containing test code, which will be type-checked */ public FlowTest(List testFiles) { diff --git a/framework/src/test/java/tests/FrameworkTest.java b/framework/src/test/java/tests/FrameworkTest.java index 5349e4d2cbf..e86d3ed813b 100644 --- a/framework/src/test/java/tests/FrameworkTest.java +++ b/framework/src/test/java/tests/FrameworkTest.java @@ -1,15 +1,15 @@ package tests; import java.io.File; -import org.checkerframework.framework.test.FrameworkPerFileTest; +import org.checkerframework.framework.test.CheckerFrameworkPerFileTest; import org.junit.runners.Parameterized.Parameters; -import testlib.util.TestChecker; +import testlib.util.EvenOddChecker; -/** JUnit tests for the Checker Framework, using the {@link TestChecker}. */ -public class FrameworkTest extends FrameworkPerFileTest { +/** JUnit tests for the Checker Framework, using the {@link EvenOddChecker}. */ +public class FrameworkTest extends CheckerFrameworkPerFileTest { public FrameworkTest(File testFile) { - super(testFile, TestChecker.class, "framework", "-Anomsgtext"); + super(testFile, EvenOddChecker.class, "framework", "-Anomsgtext"); } @Parameters diff --git a/framework/src/test/java/tests/H1H2CheckerTest.java b/framework/src/test/java/tests/H1H2CheckerTest.java new file mode 100644 index 00000000000..541130db756 --- /dev/null +++ b/framework/src/test/java/tests/H1H2CheckerTest.java @@ -0,0 +1,26 @@ +package tests; + +import h1h2checker.H1H2Checker; +import java.io.File; +import java.util.List; +import org.checkerframework.framework.test.CheckerFrameworkPerDirectoryTest; +import org.junit.runners.Parameterized.Parameters; + +/** */ +public class H1H2CheckerTest extends CheckerFrameworkPerDirectoryTest { + + /** @param testFiles the files containing test code, which will be type-checked */ + public H1H2CheckerTest(List testFiles) { + super( + testFiles, + H1H2Checker.class, + "h1h2checker", + "-Anomsgtext", + "-Astubs=tests/h1h2checker/h1h2checker.astub"); + } + + @Parameters + public static String[] getTestDirs() { + return new String[] {"h1h2checker"}; + } +} diff --git a/framework/src/test/java/tests/LubGlbTest.java b/framework/src/test/java/tests/LubGlbTest.java index 5de49380932..1377c88cb22 100644 --- a/framework/src/test/java/tests/LubGlbTest.java +++ b/framework/src/test/java/tests/LubGlbTest.java @@ -2,11 +2,11 @@ import java.io.File; import java.util.List; -import org.checkerframework.framework.test.FrameworkPerDirectoryTest; +import org.checkerframework.framework.test.CheckerFrameworkPerDirectoryTest; import org.junit.runners.Parameterized.Parameters; /** */ -public class LubGlbTest extends FrameworkPerDirectoryTest { +public class LubGlbTest extends CheckerFrameworkPerDirectoryTest { /** @param testFiles the files containing test code, which will be type-checked */ public LubGlbTest(List testFiles) { diff --git a/framework/src/test/java/tests/MethodValTest.java b/framework/src/test/java/tests/MethodValTest.java index fc187c92097..7c1901e2646 100644 --- a/framework/src/test/java/tests/MethodValTest.java +++ b/framework/src/test/java/tests/MethodValTest.java @@ -2,11 +2,11 @@ import java.io.File; import java.util.List; -import org.checkerframework.framework.test.FrameworkPerDirectoryTest; +import org.checkerframework.framework.test.CheckerFrameworkPerDirectoryTest; import org.junit.runners.Parameterized.Parameters; /** Tests the MethodVal Checker. */ -public class MethodValTest extends FrameworkPerDirectoryTest { +public class MethodValTest extends CheckerFrameworkPerDirectoryTest { /** @param testFiles the files containing test code, which will be type-checked */ public MethodValTest(List testFiles) { diff --git a/framework/src/test/java/tests/NonTopDefaultTest.java b/framework/src/test/java/tests/NonTopDefaultTest.java index 675613898ac..9fc7e9ff35b 100644 --- a/framework/src/test/java/tests/NonTopDefaultTest.java +++ b/framework/src/test/java/tests/NonTopDefaultTest.java @@ -2,12 +2,12 @@ import java.io.File; import java.util.List; -import org.checkerframework.framework.test.FrameworkPerDirectoryTest; +import org.checkerframework.framework.test.CheckerFrameworkPerDirectoryTest; import org.junit.runners.Parameterized.Parameters; import testlib.nontopdefault.NTDChecker; /** Tests the NonTopDefault Checker. */ -public class NonTopDefaultTest extends FrameworkPerDirectoryTest { +public class NonTopDefaultTest extends CheckerFrameworkPerDirectoryTest { /** @param testFiles the files containing test code, which will be type-checked */ public NonTopDefaultTest(List testFiles) { diff --git a/framework/src/test/java/tests/PuritySuggestionsTest.java b/framework/src/test/java/tests/PuritySuggestionsTest.java index e8a181008ce..1e50695c074 100644 --- a/framework/src/test/java/tests/PuritySuggestionsTest.java +++ b/framework/src/test/java/tests/PuritySuggestionsTest.java @@ -2,12 +2,12 @@ import java.io.File; import java.util.List; -import org.checkerframework.framework.test.FrameworkPerDirectoryTest; +import org.checkerframework.framework.test.CheckerFrameworkPerDirectoryTest; import org.junit.runners.Parameterized.Parameters; import testlib.util.FlowTestChecker; /** Tests for the {@code -AsuggestPureMethods} command-line argument. */ -public class PuritySuggestionsTest extends FrameworkPerDirectoryTest { +public class PuritySuggestionsTest extends CheckerFrameworkPerDirectoryTest { /** @param testFiles the files containing test code, which will be type-checked */ public PuritySuggestionsTest(List testFiles) { diff --git a/framework/src/test/java/tests/ReflectionTest.java b/framework/src/test/java/tests/ReflectionTest.java index e27a5da3b6d..146d558f19c 100644 --- a/framework/src/test/java/tests/ReflectionTest.java +++ b/framework/src/test/java/tests/ReflectionTest.java @@ -3,12 +3,12 @@ import java.io.File; import java.util.ArrayList; import java.util.List; -import org.checkerframework.framework.test.FrameworkPerDirectoryTest; +import org.checkerframework.framework.test.CheckerFrameworkPerDirectoryTest; import org.junit.runners.Parameterized.Parameters; import testlib.reflection.ReflectionTestChecker; /** Tests the reflection resolution using a simple type system. */ -public class ReflectionTest extends FrameworkPerDirectoryTest { +public class ReflectionTest extends CheckerFrameworkPerDirectoryTest { /** @param testFiles the files containing test code, which will be type-checked */ public ReflectionTest(List testFiles) { diff --git a/framework/src/test/java/tests/ReportModifiersTest.java b/framework/src/test/java/tests/ReportModifiersTest.java index 8ace1a86187..f5cf96de48b 100644 --- a/framework/src/test/java/tests/ReportModifiersTest.java +++ b/framework/src/test/java/tests/ReportModifiersTest.java @@ -2,10 +2,10 @@ import java.io.File; import java.util.List; -import org.checkerframework.framework.test.FrameworkPerDirectoryTest; +import org.checkerframework.framework.test.CheckerFrameworkPerDirectoryTest; import org.junit.runners.Parameterized.Parameters; -public class ReportModifiersTest extends FrameworkPerDirectoryTest { +public class ReportModifiersTest extends CheckerFrameworkPerDirectoryTest { /** @param testFiles the files containing test code, which will be type-checked */ public ReportModifiersTest(List testFiles) { diff --git a/framework/src/test/java/tests/ReportTest.java b/framework/src/test/java/tests/ReportTest.java index 423bbf3adf7..5c6248a64d6 100644 --- a/framework/src/test/java/tests/ReportTest.java +++ b/framework/src/test/java/tests/ReportTest.java @@ -2,10 +2,10 @@ import java.io.File; import java.util.List; -import org.checkerframework.framework.test.FrameworkPerDirectoryTest; +import org.checkerframework.framework.test.CheckerFrameworkPerDirectoryTest; import org.junit.runners.Parameterized.Parameters; -public class ReportTest extends FrameworkPerDirectoryTest { +public class ReportTest extends CheckerFrameworkPerDirectoryTest { /** @param testFiles the files containing test code, which will be type-checked */ public ReportTest(List testFiles) { diff --git a/framework/src/test/java/tests/ReportTreeKindsTest.java b/framework/src/test/java/tests/ReportTreeKindsTest.java index 6098d228e75..7db485b419d 100644 --- a/framework/src/test/java/tests/ReportTreeKindsTest.java +++ b/framework/src/test/java/tests/ReportTreeKindsTest.java @@ -2,10 +2,10 @@ import java.io.File; import java.util.List; -import org.checkerframework.framework.test.FrameworkPerDirectoryTest; +import org.checkerframework.framework.test.CheckerFrameworkPerDirectoryTest; import org.junit.runners.Parameterized.Parameters; -public class ReportTreeKindsTest extends FrameworkPerDirectoryTest { +public class ReportTreeKindsTest extends CheckerFrameworkPerDirectoryTest { /** @param testFiles the files containing test code, which will be type-checked */ public ReportTreeKindsTest(List testFiles) { diff --git a/framework/src/test/java/tests/SubtypingEncryptedTest.java b/framework/src/test/java/tests/SubtypingEncryptedTest.java index 84c50edd9a4..49e815ce684 100644 --- a/framework/src/test/java/tests/SubtypingEncryptedTest.java +++ b/framework/src/test/java/tests/SubtypingEncryptedTest.java @@ -2,11 +2,11 @@ import java.io.File; import java.util.List; -import org.checkerframework.framework.test.FrameworkPerDirectoryTest; +import org.checkerframework.framework.test.CheckerFrameworkPerDirectoryTest; import org.junit.runners.Parameterized.Parameters; /** Test suite for the Subtyping Checker, using a simple {@link Encrypted} annotation. */ -public class SubtypingEncryptedTest extends FrameworkPerDirectoryTest { +public class SubtypingEncryptedTest extends CheckerFrameworkPerDirectoryTest { /** @param testFiles the files containing test code, which will be type-checked */ public SubtypingEncryptedTest(List testFiles) { diff --git a/framework/src/test/java/tests/SubtypingStringPatternsFullTest.java b/framework/src/test/java/tests/SubtypingStringPatternsFullTest.java index 821cfa99e9d..a48a8634fdb 100644 --- a/framework/src/test/java/tests/SubtypingStringPatternsFullTest.java +++ b/framework/src/test/java/tests/SubtypingStringPatternsFullTest.java @@ -2,11 +2,11 @@ import java.io.File; import java.util.List; -import org.checkerframework.framework.test.FrameworkPerDirectoryTest; +import org.checkerframework.framework.test.CheckerFrameworkPerDirectoryTest; import org.junit.runners.Parameterized.Parameters; /** Test suite for the Subtyping Checker, using a simple {@link Encrypted} annotation. */ -public class SubtypingStringPatternsFullTest extends FrameworkPerDirectoryTest { +public class SubtypingStringPatternsFullTest extends CheckerFrameworkPerDirectoryTest { /** @param testFiles the files containing test code, which will be type-checked */ public SubtypingStringPatternsFullTest(List testFiles) { diff --git a/framework/src/test/java/tests/SubtypingStringPatternsPartialTest.java b/framework/src/test/java/tests/SubtypingStringPatternsPartialTest.java index c9e5bb182eb..592b2b06fe9 100644 --- a/framework/src/test/java/tests/SubtypingStringPatternsPartialTest.java +++ b/framework/src/test/java/tests/SubtypingStringPatternsPartialTest.java @@ -2,11 +2,11 @@ import java.io.File; import java.util.List; -import org.checkerframework.framework.test.FrameworkPerDirectoryTest; +import org.checkerframework.framework.test.CheckerFrameworkPerDirectoryTest; import org.junit.runners.Parameterized.Parameters; /** Test suite for the Subtyping Checker, using a simple {@link Encrypted} annotation. */ -public class SubtypingStringPatternsPartialTest extends FrameworkPerDirectoryTest { +public class SubtypingStringPatternsPartialTest extends CheckerFrameworkPerDirectoryTest { /** @param testFiles the files containing test code, which will be type-checked */ public SubtypingStringPatternsPartialTest(List testFiles) { diff --git a/framework/src/test/java/tests/SupportedQualsTest.java b/framework/src/test/java/tests/SupportedQualsTest.java index 7e3053f3a3c..b0ef0dab9e0 100644 --- a/framework/src/test/java/tests/SupportedQualsTest.java +++ b/framework/src/test/java/tests/SupportedQualsTest.java @@ -2,11 +2,11 @@ import java.io.File; import java.util.List; -import org.checkerframework.framework.test.FrameworkPerDirectoryTest; +import org.checkerframework.framework.test.CheckerFrameworkPerDirectoryTest; import org.junit.runners.Parameterized.Parameters; import testlib.supportedquals.SupportedQualsChecker; -public class SupportedQualsTest extends FrameworkPerDirectoryTest { +public class SupportedQualsTest extends CheckerFrameworkPerDirectoryTest { /** @param testFiles the files containing test code, which will be type-checked */ public SupportedQualsTest(List testFiles) { diff --git a/framework/src/test/java/tests/TestCheckerTest.java b/framework/src/test/java/tests/TestCheckerTest.java deleted file mode 100644 index 7f5e07e2e85..00000000000 --- a/framework/src/test/java/tests/TestCheckerTest.java +++ /dev/null @@ -1,26 +0,0 @@ -package tests; - -import java.io.File; -import java.util.List; -import org.checkerframework.framework.test.FrameworkPerDirectoryTest; -import org.junit.runners.Parameterized.Parameters; -import testchecker.TestChecker; - -/** */ -public class TestCheckerTest extends FrameworkPerDirectoryTest { - - /** @param testFiles the files containing test code, which will be type-checked */ - public TestCheckerTest(List testFiles) { - super( - testFiles, - TestChecker.class, - "testchecker", - "-Anomsgtext", - "-Astubs=tests/testchecker/testchecker.astub"); - } - - @Parameters - public static String[] getTestDirs() { - return new String[] {"testchecker"}; - } -} diff --git a/framework/src/test/java/tests/TypeDeclDefaultTest.java b/framework/src/test/java/tests/TypeDeclDefaultTest.java index ccca47715e0..1951ea309d6 100644 --- a/framework/src/test/java/tests/TypeDeclDefaultTest.java +++ b/framework/src/test/java/tests/TypeDeclDefaultTest.java @@ -2,11 +2,11 @@ import java.io.File; import java.util.List; -import org.checkerframework.framework.test.FrameworkPerDirectoryTest; +import org.checkerframework.framework.test.CheckerFrameworkPerDirectoryTest; import org.junit.runners.Parameterized.Parameters; /** Create the TypeDeclDefault test. */ -public class TypeDeclDefaultTest extends FrameworkPerDirectoryTest { +public class TypeDeclDefaultTest extends CheckerFrameworkPerDirectoryTest { /** @param testFiles the files containing test code, which will be type-checked */ public TypeDeclDefaultTest(List testFiles) { diff --git a/framework/src/test/java/tests/ValueIgnoreRangeOverflowTest.java b/framework/src/test/java/tests/ValueIgnoreRangeOverflowTest.java index 093684ab379..c7b7977ac5f 100644 --- a/framework/src/test/java/tests/ValueIgnoreRangeOverflowTest.java +++ b/framework/src/test/java/tests/ValueIgnoreRangeOverflowTest.java @@ -3,11 +3,11 @@ import java.io.File; import java.util.List; import org.checkerframework.common.value.ValueChecker; -import org.checkerframework.framework.test.FrameworkPerDirectoryTest; +import org.checkerframework.framework.test.CheckerFrameworkPerDirectoryTest; import org.junit.runners.Parameterized.Parameters; /** Tests the constant value propagation type system without overflow. */ -public class ValueIgnoreRangeOverflowTest extends FrameworkPerDirectoryTest { +public class ValueIgnoreRangeOverflowTest extends CheckerFrameworkPerDirectoryTest { /** @param testFiles the files containing test code, which will be type-checked */ public ValueIgnoreRangeOverflowTest(List testFiles) { diff --git a/framework/src/test/java/tests/ValueNonNullStringsConcatenationTest.java b/framework/src/test/java/tests/ValueNonNullStringsConcatenationTest.java index 47f560ee05b..d681e58565b 100644 --- a/framework/src/test/java/tests/ValueNonNullStringsConcatenationTest.java +++ b/framework/src/test/java/tests/ValueNonNullStringsConcatenationTest.java @@ -3,10 +3,10 @@ import java.io.File; import java.util.List; import org.checkerframework.common.value.ValueChecker; -import org.checkerframework.framework.test.FrameworkPerDirectoryTest; +import org.checkerframework.framework.test.CheckerFrameworkPerDirectoryTest; import org.junit.runners.Parameterized.Parameters; -public class ValueNonNullStringsConcatenationTest extends FrameworkPerDirectoryTest { +public class ValueNonNullStringsConcatenationTest extends CheckerFrameworkPerDirectoryTest { /** @param testFiles the files containing test code, which will be type-checked */ public ValueNonNullStringsConcatenationTest(List testFiles) { diff --git a/framework/src/test/java/tests/ValueTest.java b/framework/src/test/java/tests/ValueTest.java index 38cac432432..d9f3d268d0d 100644 --- a/framework/src/test/java/tests/ValueTest.java +++ b/framework/src/test/java/tests/ValueTest.java @@ -3,7 +3,7 @@ import java.io.File; import java.util.List; import org.checkerframework.common.value.ValueChecker; -import org.checkerframework.framework.test.FrameworkPerDirectoryTest; +import org.checkerframework.framework.test.CheckerFrameworkPerDirectoryTest; import org.junit.runners.Parameterized.Parameters; /** @@ -13,7 +13,7 @@ * ExceptionTest will fail because it cannot find the ExceptionTest.class file for reflective method * resolution. */ -public class ValueTest extends FrameworkPerDirectoryTest { +public class ValueTest extends CheckerFrameworkPerDirectoryTest { /** @param testFiles the files containing test code, which will be type-checked */ public ValueTest(List testFiles) { diff --git a/framework/src/test/java/tests/ValueUncheckedDefaultsTest.java b/framework/src/test/java/tests/ValueUncheckedDefaultsTest.java index 6cc64ec1261..bb9e614b95c 100644 --- a/framework/src/test/java/tests/ValueUncheckedDefaultsTest.java +++ b/framework/src/test/java/tests/ValueUncheckedDefaultsTest.java @@ -3,11 +3,11 @@ import java.io.File; import java.util.List; import org.checkerframework.common.value.ValueChecker; -import org.checkerframework.framework.test.FrameworkPerDirectoryTest; +import org.checkerframework.framework.test.CheckerFrameworkPerDirectoryTest; import org.junit.runners.Parameterized.Parameters; /** Tests conservative defaults for the constant value propagation type system. */ -public class ValueUncheckedDefaultsTest extends FrameworkPerDirectoryTest { +public class ValueUncheckedDefaultsTest extends CheckerFrameworkPerDirectoryTest { /** @param testFiles the files containing test code, which will be type-checked */ public ValueUncheckedDefaultsTest(List testFiles) { diff --git a/framework/src/test/java/tests/wpirunners/WholeProgramInferenceJaifsTest.java b/framework/src/test/java/tests/wpirunners/WholeProgramInferenceJaifsTest.java index 86fffd209b4..796e68187bf 100644 --- a/framework/src/test/java/tests/wpirunners/WholeProgramInferenceJaifsTest.java +++ b/framework/src/test/java/tests/wpirunners/WholeProgramInferenceJaifsTest.java @@ -2,7 +2,7 @@ import java.io.File; import java.util.List; -import org.checkerframework.framework.test.FrameworkPerDirectoryTest; +import org.checkerframework.framework.test.CheckerFrameworkPerDirectoryTest; import org.junit.experimental.categories.Category; import org.junit.runners.Parameterized.Parameters; import testlib.wholeprograminference.WholeProgramInferenceTestChecker; @@ -15,7 +15,7 @@ * the expected ones. The errors on .java files must be ignored. */ @Category(WholeProgramInferenceJaifsTest.class) -public class WholeProgramInferenceJaifsTest extends FrameworkPerDirectoryTest { +public class WholeProgramInferenceJaifsTest extends CheckerFrameworkPerDirectoryTest { /** @param testFiles the files containing test code, which will be type-checked */ public WholeProgramInferenceJaifsTest(List testFiles) { super( diff --git a/framework/src/test/java/tests/wpirunners/WholeProgramInferenceJaifsValidationTest.java b/framework/src/test/java/tests/wpirunners/WholeProgramInferenceJaifsValidationTest.java index 0997832db90..8c965426b5c 100644 --- a/framework/src/test/java/tests/wpirunners/WholeProgramInferenceJaifsValidationTest.java +++ b/framework/src/test/java/tests/wpirunners/WholeProgramInferenceJaifsValidationTest.java @@ -2,7 +2,7 @@ import java.io.File; import java.util.List; -import org.checkerframework.framework.test.FrameworkPerDirectoryTest; +import org.checkerframework.framework.test.CheckerFrameworkPerDirectoryTest; import org.junit.experimental.categories.Category; import org.junit.runners.Parameterized.Parameters; import testlib.wholeprograminference.WholeProgramInferenceTestChecker; @@ -12,7 +12,7 @@ * which ensures that with the annotations inserted, the errors are no longer issued. */ @Category(WholeProgramInferenceJaifsTest.class) -public class WholeProgramInferenceJaifsValidationTest extends FrameworkPerDirectoryTest { +public class WholeProgramInferenceJaifsValidationTest extends CheckerFrameworkPerDirectoryTest { /** @param testFiles the files containing test code, which will be type-checked */ public WholeProgramInferenceJaifsValidationTest(List testFiles) { super(testFiles, WholeProgramInferenceTestChecker.class, "value", "-Anomsgtext"); diff --git a/framework/src/test/java/tests/wpirunners/WholeProgramInferenceStubsTest.java b/framework/src/test/java/tests/wpirunners/WholeProgramInferenceStubsTest.java index df0a1233feb..0803dde032d 100644 --- a/framework/src/test/java/tests/wpirunners/WholeProgramInferenceStubsTest.java +++ b/framework/src/test/java/tests/wpirunners/WholeProgramInferenceStubsTest.java @@ -2,7 +2,7 @@ import java.io.File; import java.util.List; -import org.checkerframework.framework.test.FrameworkPerDirectoryTest; +import org.checkerframework.framework.test.CheckerFrameworkPerDirectoryTest; import org.junit.experimental.categories.Category; import org.junit.runners.Parameterized.Parameters; import testlib.wholeprograminference.WholeProgramInferenceTestChecker; @@ -16,7 +16,7 @@ * the expected ones. The errors on .java files must be ignored. */ @Category(WholeProgramInferenceStubsTest.class) -public class WholeProgramInferenceStubsTest extends FrameworkPerDirectoryTest { +public class WholeProgramInferenceStubsTest extends CheckerFrameworkPerDirectoryTest { /** @param testFiles the files containing test code, which will be type-checked */ public WholeProgramInferenceStubsTest(List testFiles) { diff --git a/framework/src/test/java/tests/wpirunners/WholeProgramInferenceStubsValidationTest.java b/framework/src/test/java/tests/wpirunners/WholeProgramInferenceStubsValidationTest.java index 2f38acfdbad..3582531d2cf 100644 --- a/framework/src/test/java/tests/wpirunners/WholeProgramInferenceStubsValidationTest.java +++ b/framework/src/test/java/tests/wpirunners/WholeProgramInferenceStubsValidationTest.java @@ -2,7 +2,7 @@ import java.io.File; import java.util.List; -import org.checkerframework.framework.test.FrameworkPerDirectoryTest; +import org.checkerframework.framework.test.CheckerFrameworkPerDirectoryTest; import org.junit.experimental.categories.Category; import org.junit.runners.Parameterized.Parameters; import testlib.wholeprograminference.WholeProgramInferenceTestChecker; @@ -12,7 +12,7 @@ * that with the stubs in place, the errors that those annotations remove are no longer issued. */ @Category(WholeProgramInferenceStubsTest.class) -public class WholeProgramInferenceStubsValidationTest extends FrameworkPerDirectoryTest { +public class WholeProgramInferenceStubsValidationTest extends CheckerFrameworkPerDirectoryTest { /** @param testFiles the files containing test code, which will be type-checked */ public WholeProgramInferenceStubsValidationTest(List testFiles) { diff --git a/framework/tests/accumulation/Not.java b/framework/tests/accumulation/Not.java new file mode 100644 index 00000000000..4bcbacb87a6 --- /dev/null +++ b/framework/tests/accumulation/Not.java @@ -0,0 +1,75 @@ +import testaccumulation.qual.*; + +class Not { + + class Foo { + void a() {} + + void b() {} + + void c() {} + + void notA(@TestAccumulationPredicate("!a") Foo this) {} + + void notB(@TestAccumulationPredicate("!b") Foo this) {} + } + + void test1(Foo f) { + f.notA(); + f.notB(); + } + + void test2(Foo f) { + f.c(); + f.notA(); + f.notB(); + } + + void test3(Foo f) { + f.a(); + // :: error: method.invocation.invalid + f.notA(); + f.notB(); + } + + void test4(Foo f) { + f.b(); + f.notA(); + // :: error: method.invocation.invalid + f.notB(); + } + + void test5(Foo f) { + f.a(); + f.b(); + // :: error: method.invocation.invalid + f.notA(); + // :: error: method.invocation.invalid + f.notB(); + } + + void callA(Foo f) { + f.a(); + } + + void test6(Foo f) { + callA(f); + // DEMONSTRATION OF "UNSOUNDNESS" + f.notA(); + } + + void test7(@TestAccumulation("a") Foo f) { + // :: error: method.invocation.invalid + f.notA(); + } + + void test8(Foo f, boolean test) { + if (test) { + f.a(); + } else { + f.b(); + } + // DEMONSTRATION OF "UNSOUNDNESS" + f.notA(); + } +} diff --git a/framework/tests/accumulation/Predicates.java b/framework/tests/accumulation/Predicates.java new file mode 100644 index 00000000000..0aec31dae32 --- /dev/null +++ b/framework/tests/accumulation/Predicates.java @@ -0,0 +1,634 @@ +import testaccumulation.qual.*; + +class Predicates { + + void testOr1() { + MyClass m1 = new MyClass(); + + // :: error: method.invocation.invalid + m1.c(); + } + + void testOr2() { + MyClass m1 = new MyClass(); + + m1.a(); + m1.c(); + } + + void testOr3() { + MyClass m1 = new MyClass(); + + m1.b(); + m1.c(); + } + + void testAnd1() { + MyClass m1 = new MyClass(); + + // :: error: method.invocation.invalid + m1.d(); + } + + void testAnd2() { + MyClass m1 = new MyClass(); + + m1.a(); + // :: error: method.invocation.invalid + m1.d(); + } + + void testAnd3() { + MyClass m1 = new MyClass(); + + m1.b(); + // :: error: method.invocation.invalid + m1.d(); + } + + void testAnd4() { + MyClass m1 = new MyClass(); + + m1.a(); + m1.c(); + // :: error: method.invocation.invalid + m1.d(); + } + + void testAnd5() { + MyClass m1 = new MyClass(); + + m1.a(); + m1.b(); + m1.d(); + } + + void testAnd6() { + MyClass m1 = new MyClass(); + + m1.a(); + m1.b(); + m1.c(); + m1.d(); + } + + void testAndOr1() { + MyClass m1 = new MyClass(); + + // :: error: method.invocation.invalid + m1.e(); + } + + void testAndOr2() { + MyClass m1 = new MyClass(); + + m1.a(); + m1.e(); + } + + void testAndOr3() { + MyClass m1 = new MyClass(); + + m1.b(); + // :: error: method.invocation.invalid + m1.e(); + } + + void testAndOr4() { + MyClass m1 = new MyClass(); + + m1.b(); + m1.c(); + m1.e(); + } + + void testAndOr5() { + MyClass m1 = new MyClass(); + + m1.a(); + m1.b(); + m1.c(); + m1.d(); + m1.e(); + } + + void testPrecedence1() { + MyClass m1 = new MyClass(); + + // :: error: method.invocation.invalid + m1.f(); + } + + void testPrecedence2() { + MyClass m1 = new MyClass(); + + m1.a(); + // :: error: method.invocation.invalid + m1.f(); + } + + void testPrecedence3() { + MyClass m1 = new MyClass(); + + m1.b(); + // :: error: method.invocation.invalid + m1.f(); + } + + void testPrecedence4() { + MyClass m1 = new MyClass(); + + m1.a(); + m1.b(); + m1.f(); + } + + void testPrecedence5() { + MyClass m1 = new MyClass(); + + m1.a(); + m1.c(); + m1.f(); + } + + void testPrecedence6() { + MyClass m1 = new MyClass(); + + m1.b(); + m1.c(); + m1.f(); + } + + private static class MyClass { + + @TestAccumulation("a") + MyClass cmA; + + @TestAccumulationPredicate("a") + MyClass cmpA; + + @TestAccumulation({"a", "b"}) + MyClass aB; + + @TestAccumulationPredicate("a || b") + MyClass aOrB; + + @TestAccumulationPredicate("a && b") + MyClass aAndB; + + @TestAccumulationPredicate("a || b && c") + MyClass bAndCOrA; + + @TestAccumulationPredicate("a || (b && c)") + MyClass bAndCOrAParens; + + @TestAccumulationPredicate("a && b || c") + MyClass aAndBOrC; + + @TestAccumulationPredicate("(a && b) || c") + MyClass aAndBOrCParens; + + @TestAccumulationPredicate("(a || b) && c") + MyClass aOrBAndC; + + @TestAccumulationPredicate("a && (b || c)") + MyClass bOrCAndA; + + @TestAccumulationPredicate("b && c") + MyClass bAndC; + + @TestAccumulationPredicate("(b && c)") + MyClass bAndCParens; + + void a() {} + + void b() {} + + void c(@TestAccumulationPredicate("a || b") MyClass this) {} + + void d(@TestAccumulationPredicate("a && b") MyClass this) {} + + void e(@TestAccumulationPredicate("a || (b && c)") MyClass this) {} + + void f(@TestAccumulationPredicate("a && b || c") MyClass this) {} + + static void testAssignability1(@TestAccumulationPredicate("a || b") MyClass cAble) { + cAble.c(); + // :: error: method.invocation.invalid + cAble.d(); + // :: error: method.invocation.invalid + cAble.e(); + // :: error: method.invocation.invalid + cAble.f(); + } + + static void testAssignability2(@TestAccumulationPredicate("a && b") MyClass dAble) { + // These calls would work if subtyping between predicates was by implication. They issue + // errors, because + // it is not. + // :: error: method.invocation.invalid + dAble.c(); + dAble.d(); + // :: error: method.invocation.invalid + dAble.e(); + // :: error: method.invocation.invalid + dAble.f(); + } + + void testAllAssignability() { + + @TestAccumulation("a") + MyClass cmALocal; + @TestAccumulationPredicate("a") + MyClass cmpALocal; + @TestAccumulationPredicate("a || b") + MyClass aOrBLocal; + @TestAccumulation({"a", "b"}) + MyClass aBLocal; + @TestAccumulationPredicate("a && b") + MyClass aAndBLocal; + @TestAccumulationPredicate("a || b && c") + MyClass bAndCOrALocal; + @TestAccumulationPredicate("a || (b && c)") + MyClass bAndCOrAParensLocal; + @TestAccumulationPredicate("a && b || c") + MyClass aAndBOrCLocal; + @TestAccumulationPredicate("(a && b) || c") + MyClass aAndBOrCParensLocal; + @TestAccumulationPredicate("(a || b) && c") + MyClass aOrBAndCLocal; + @TestAccumulationPredicate("a && (b || c)") + MyClass bOrCAndALocal; + @TestAccumulationPredicate("b && c") + MyClass bAndCLocal; + @TestAccumulationPredicate("(b && c)") + MyClass bAndCParensLocal; + + cmALocal = cmA; + cmALocal = cmpA; + // :: error: assignment.type.incompatible + cmALocal = aOrB; + cmALocal = aB; + // The next line would not fail if predicate subtyping was decided by implication. + // :: error: assignment.type.incompatible + cmALocal = aAndB; + // :: error: assignment.type.incompatible + cmALocal = bAndCOrA; + // :: error: assignment.type.incompatible + cmALocal = bAndCOrAParens; + // :: error: assignment.type.incompatible + cmALocal = aAndBOrC; + // :: error: assignment.type.incompatible + cmALocal = aAndBOrCParens; + // :: error: assignment.type.incompatible + cmALocal = aOrBAndC; + // The next line would not fail if predicate subtyping was decided by implication. + // :: error: assignment.type.incompatible + cmALocal = bOrCAndA; + // :: error: assignment.type.incompatible + cmALocal = bAndC; + // :: error: assignment.type.incompatible + cmALocal = bAndCParens; + + cmpALocal = cmA; + cmpALocal = cmpA; + // :: error: assignment.type.incompatible + cmpALocal = aOrB; + cmpALocal = aB; + // The next line would not fail if predicate subtyping was decided by implication. + // :: error: assignment.type.incompatible + cmpALocal = aAndB; + // :: error: assignment.type.incompatible + cmpALocal = bAndCOrA; + // :: error: assignment.type.incompatible + cmpALocal = bAndCOrAParens; + // :: error: assignment.type.incompatible + cmpALocal = aAndBOrC; + // :: error: assignment.type.incompatible + cmpALocal = aAndBOrCParens; + // :: error: assignment.type.incompatible + cmpALocal = aOrBAndC; + // The next line would not fail if predicate subtyping was decided by implication. + // :: error: assignment.type.incompatible + cmpALocal = bOrCAndA; + // :: error: assignment.type.incompatible + cmpALocal = bAndC; + // :: error: assignment.type.incompatible + cmpALocal = bAndCParens; + + aOrBLocal = cmA; + // The next line would not fail if predicate subtyping was decided by implication. + // :: error: assignment.type.incompatible + aOrBLocal = cmpA; + aOrBLocal = aOrB; + aOrBLocal = aB; + // The next line would not fail if predicate subtyping was decided by implication. + // :: error: assignment.type.incompatible + aOrBLocal = aAndB; + // The next line would not fail if predicate subtyping was decided by implication. + // :: error: assignment.type.incompatible + aOrBLocal = bAndCOrA; + // The next line would not fail if predicate subtyping was decided by implication. + // :: error: assignment.type.incompatible + aOrBLocal = bAndCOrAParens; + // :: error: assignment.type.incompatible + aOrBLocal = aAndBOrC; + // :: error: assignment.type.incompatible + aOrBLocal = aAndBOrCParens; + // The next line would not fail if predicate subtyping was decided by implication. + // :: error: assignment.type.incompatible + aOrBLocal = aOrBAndC; + // The next line would not fail if predicate subtyping was decided by implication. + // :: error: assignment.type.incompatible + aOrBLocal = bOrCAndA; + // The next line would not fail if predicate subtyping was decided by implication. + // :: error: assignment.type.incompatible + aOrBLocal = bAndC; + // The next line would not fail if predicate subtyping was decided by implication. + // :: error: assignment.type.incompatible + aOrBLocal = bAndCParens; + + // :: error: (assignment.type.incompatible) + aBLocal = cmA; + // :: error: (assignment.type.incompatible) + aBLocal = cmpA; + // :: error: (assignment.type.incompatible) + aBLocal = aOrB; + aBLocal = aB; + aBLocal = aAndB; + // :: error: (assignment.type.incompatible) + aBLocal = bAndCOrA; + // :: error: (assignment.type.incompatible) + aBLocal = bAndCOrAParens; + // :: error: (assignment.type.incompatible) + aBLocal = aAndBOrC; + // :: error: (assignment.type.incompatible) + aBLocal = aAndBOrCParens; + // :: error: (assignment.type.incompatible) + aBLocal = aOrBAndC; + // :: error: (assignment.type.incompatible) + aBLocal = bOrCAndA; + // :: error: (assignment.type.incompatible) + aBLocal = bAndC; + // :: error: (assignment.type.incompatible) + aBLocal = bAndCParens; + + // :: error: (assignment.type.incompatible) + aAndBLocal = cmA; + // :: error: (assignment.type.incompatible) + aAndBLocal = cmpA; + // :: error: (assignment.type.incompatible) + aAndBLocal = aOrB; + aAndBLocal = aB; + aAndBLocal = aAndB; + // :: error: (assignment.type.incompatible) + aAndBLocal = bAndCOrA; + // :: error: (assignment.type.incompatible) + aAndBLocal = bAndCOrAParens; + // :: error: (assignment.type.incompatible) + aAndBLocal = aAndBOrC; + // :: error: (assignment.type.incompatible) + aAndBLocal = aAndBOrCParens; + // :: error: (assignment.type.incompatible) + aAndBLocal = aOrBAndC; + // :: error: (assignment.type.incompatible) + aAndBLocal = bOrCAndA; + // :: error: (assignment.type.incompatible) + aAndBLocal = bAndC; + // :: error: (assignment.type.incompatible) + aAndBLocal = bAndCParens; + + bAndCOrALocal = cmA; + // The next line would not fail if predicate subtyping was decided by implication. + // :: error: assignment.type.incompatible + bAndCOrALocal = cmpA; + // :: error: (assignment.type.incompatible) + bAndCOrALocal = aOrB; + bAndCOrALocal = aB; + // The next line would not fail if predicate subtyping was decided by implication. + // :: error: assignment.type.incompatible + bAndCOrALocal = aAndB; + bAndCOrALocal = bAndCOrA; + // The next line would not fail if predicate subtyping was decided by implication. + // :: error: assignment.type.incompatible + bAndCOrALocal = bAndCOrAParens; + // :: error: (assignment.type.incompatible) + bAndCOrALocal = aAndBOrC; + // :: error: (assignment.type.incompatible) + bAndCOrALocal = aAndBOrCParens; + // The next line would not fail if predicate subtyping was decided by implication. + // :: error: assignment.type.incompatible + bAndCOrALocal = aOrBAndC; + // The next line would not fail if predicate subtyping was decided by implication. + // :: error: assignment.type.incompatible + bAndCOrALocal = bOrCAndA; + // The next line would not fail if predicate subtyping was decided by implication. + // :: error: assignment.type.incompatible + bAndCOrALocal = bAndC; + // The next line would not fail if predicate subtyping was decided by implication. + // :: error: assignment.type.incompatible + bAndCOrALocal = bAndCParens; + + bAndCOrAParensLocal = cmA; + // The next line would not fail if predicate subtyping was decided by implication. + // :: error: assignment.type.incompatible + bAndCOrAParensLocal = cmpA; + // :: error: (assignment.type.incompatible) + bAndCOrAParensLocal = aOrB; + bAndCOrAParensLocal = aB; + // The next line would not fail if predicate subtyping was decided by implication. + // :: error: assignment.type.incompatible + bAndCOrAParensLocal = aAndB; + // The next line would not fail if predicate subtyping was decided by implication. + // :: error: assignment.type.incompatible + bAndCOrAParensLocal = bAndCOrA; + bAndCOrAParensLocal = bAndCOrAParens; + // :: error: (assignment.type.incompatible) + bAndCOrAParensLocal = aAndBOrC; + // :: error: (assignment.type.incompatible) + bAndCOrAParensLocal = aAndBOrCParens; + // The next line would not fail if predicate subtyping was decided by implication. + // :: error: assignment.type.incompatible + bAndCOrAParensLocal = aOrBAndC; + // The next line would not fail if predicate subtyping was decided by implication. + // :: error: assignment.type.incompatible + bAndCOrAParensLocal = bOrCAndA; + // The next line would not fail if predicate subtyping was decided by implication. + // :: error: assignment.type.incompatible + bAndCOrAParensLocal = bAndC; + // The next line would not fail if predicate subtyping was decided by implication. + // :: error: assignment.type.incompatible + bAndCOrAParensLocal = bAndCParens; + + // :: error: (assignment.type.incompatible) + aAndBOrCLocal = cmA; + // :: error: (assignment.type.incompatible) + aAndBOrCLocal = cmpA; + // :: error: (assignment.type.incompatible) + aAndBOrCLocal = aOrB; + aAndBOrCLocal = aB; + // The next line would not fail if predicate subtyping was decided by implication. + // :: error: assignment.type.incompatible + aAndBOrCLocal = aAndB; + // :: error: (assignment.type.incompatible) + aAndBOrCLocal = bAndCOrA; + // :: error: (assignment.type.incompatible) + aAndBOrCLocal = bAndCOrAParens; + aAndBOrCLocal = aAndBOrC; + // The next line would not fail if predicate subtyping was decided by implication. + // :: error: assignment.type.incompatible + aAndBOrCLocal = aAndBOrCParens; + // The next line would not fail if predicate subtyping was decided by implication. + // :: error: assignment.type.incompatible + aAndBOrCLocal = aOrBAndC; + // The next line would not fail if predicate subtyping was decided by implication. + // :: error: assignment.type.incompatible + aAndBOrCLocal = bOrCAndA; + // The next line would not fail if predicate subtyping was decided by implication. + // :: error: assignment.type.incompatible + aAndBOrCLocal = bAndC; + // The next line would not fail if predicate subtyping was decided by implication. + // :: error: assignment.type.incompatible + aAndBOrCLocal = bAndCParens; + + // :: error: (assignment.type.incompatible) + aAndBOrCParensLocal = cmA; + // :: error: (assignment.type.incompatible) + aAndBOrCParensLocal = cmpA; + // :: error: (assignment.type.incompatible) + aAndBOrCParensLocal = aOrB; + aAndBOrCParensLocal = aB; + // The next line would not fail if predicate subtyping was decided by implication. + // :: error: assignment.type.incompatible + aAndBOrCParensLocal = aAndB; + // :: error: (assignment.type.incompatible) + aAndBOrCParensLocal = bAndCOrA; + // :: error: (assignment.type.incompatible) + aAndBOrCParensLocal = bAndCOrAParens; + // The next line would not fail if predicate subtyping was decided by implication. + // :: error: assignment.type.incompatible + aAndBOrCParensLocal = aAndBOrC; + aAndBOrCParensLocal = aAndBOrCParens; + // The next line would not fail if predicate subtyping was decided by implication. + // :: error: assignment.type.incompatible + aAndBOrCParensLocal = aOrBAndC; + // The next line would not fail if predicate subtyping was decided by implication. + // :: error: assignment.type.incompatible + aAndBOrCParensLocal = bOrCAndA; + // The next line would not fail if predicate subtyping was decided by implication. + // :: error: assignment.type.incompatible + aAndBOrCParensLocal = bAndC; + // The next line would not fail if predicate subtyping was decided by implication. + // :: error: assignment.type.incompatible + aAndBOrCParensLocal = bAndCParens; + + // :: error: (assignment.type.incompatible) + aOrBAndCLocal = cmA; + // :: error: (assignment.type.incompatible) + aOrBAndCLocal = cmpA; + // :: error: (assignment.type.incompatible) + aOrBAndCLocal = aOrB; + // :: error: (assignment.type.incompatible) + aOrBAndCLocal = aB; + // :: error: (assignment.type.incompatible) + aOrBAndCLocal = aAndB; + // :: error: (assignment.type.incompatible) + aOrBAndCLocal = bAndCOrA; + // :: error: (assignment.type.incompatible) + aOrBAndCLocal = bAndCOrAParens; + // :: error: (assignment.type.incompatible) + aOrBAndCLocal = aAndBOrC; + // :: error: (assignment.type.incompatible) + aOrBAndCLocal = aAndBOrCParens; + aOrBAndCLocal = aOrBAndC; + // :: error: (assignment.type.incompatible) + aOrBAndCLocal = bOrCAndA; + // The next line would not fail if predicate subtyping was decided by implication. + // :: error: assignment.type.incompatible + aOrBAndCLocal = bAndC; + // The next line would not fail if predicate subtyping was decided by implication. + // :: error: assignment.type.incompatible + aOrBAndCLocal = bAndCParens; + + // :: error: (assignment.type.incompatible) + bOrCAndALocal = cmA; + // :: error: (assignment.type.incompatible) + bOrCAndALocal = cmpA; + // :: error: (assignment.type.incompatible) + bOrCAndALocal = aOrB; + bOrCAndALocal = aB; + // The next line would not fail if predicate subtyping was decided by implication. + // :: error: assignment.type.incompatible + bOrCAndALocal = aAndB; + // :: error: (assignment.type.incompatible) + bOrCAndALocal = bAndCOrA; + // :: error: (assignment.type.incompatible) + bOrCAndALocal = bAndCOrAParens; + // :: error: (assignment.type.incompatible) + bOrCAndALocal = aAndBOrC; + // :: error: (assignment.type.incompatible) + bOrCAndALocal = aAndBOrCParens; + // :: error: (assignment.type.incompatible) + bOrCAndALocal = aOrBAndC; + bOrCAndALocal = bOrCAndA; + // :: error: (assignment.type.incompatible) + bOrCAndALocal = bAndC; + // :: error: (assignment.type.incompatible) + bOrCAndALocal = bAndCParens; + + // :: error: (assignment.type.incompatible) + bAndCLocal = cmA; + // :: error: (assignment.type.incompatible) + bAndCLocal = cmpA; + // :: error: (assignment.type.incompatible) + bAndCLocal = aOrB; + // :: error: (assignment.type.incompatible) + bAndCLocal = aB; + // :: error: (assignment.type.incompatible) + bAndCLocal = aAndB; + // :: error: (assignment.type.incompatible) + bAndCLocal = bAndCOrA; + // :: error: (assignment.type.incompatible) + bAndCLocal = bAndCOrAParens; + // :: error: (assignment.type.incompatible) + bAndCLocal = aAndBOrC; + // :: error: (assignment.type.incompatible) + bAndCLocal = aAndBOrCParens; + // :: error: (assignment.type.incompatible) + bAndCLocal = aOrBAndC; + // :: error: (assignment.type.incompatible) + bAndCLocal = bOrCAndA; + bAndCLocal = bAndC; + // The next line would not fail if predicate subtyping was decided by implication. + // :: error: assignment.type.incompatible + bAndCLocal = bAndCParens; + + // :: error: (assignment.type.incompatible) + bAndCParensLocal = cmA; + // :: error: (assignment.type.incompatible) + bAndCParensLocal = cmpA; + // :: error: (assignment.type.incompatible) + bAndCParensLocal = aOrB; + // :: error: (assignment.type.incompatible) + bAndCParensLocal = aB; + // :: error: (assignment.type.incompatible) + bAndCParensLocal = aAndB; + // :: error: (assignment.type.incompatible) + bAndCParensLocal = bAndCOrA; + // :: error: (assignment.type.incompatible) + bAndCParensLocal = bAndCOrAParens; + // :: error: (assignment.type.incompatible) + bAndCParensLocal = aAndBOrC; + // :: error: (assignment.type.incompatible) + bAndCParensLocal = aAndBOrCParens; + // :: error: (assignment.type.incompatible) + bAndCParensLocal = aOrBAndC; + // :: error: (assignment.type.incompatible) + bAndCParensLocal = bOrCAndA; + // The next line would not fail if predicate subtyping was decided by implication. + // :: error: assignment.type.incompatible + bAndCParensLocal = bAndC; + bAndCParensLocal = bAndCParens; + } + } +} diff --git a/framework/tests/accumulation/SimpleInference.java b/framework/tests/accumulation/SimpleInference.java index cb4f9bf5aa2..7e6600c0a7b 100644 --- a/framework/tests/accumulation/SimpleInference.java +++ b/framework/tests/accumulation/SimpleInference.java @@ -3,17 +3,35 @@ class SimpleInference { void build(@TestAccumulation({"a"}) SimpleInference this) {} + void doublebuild(@TestAccumulation({"a", "b"}) SimpleInference this) {} + void a() {} + void b() {} + static void doStuffCorrect() { SimpleInference s = new SimpleInference(); s.a(); s.build(); } + static void doStuffCorrect2() { + SimpleInference s = new SimpleInference(); + s.a(); + s.b(); + s.doublebuild(); + } + static void doStuffWrong() { SimpleInference s = new SimpleInference(); // :: error: method.invocation.invalid s.build(); } + + static void doStuffWrong2() { + SimpleInference s = new SimpleInference(); + s.a(); + // :: error: method.invocation.invalid + s.doublebuild(); + } } diff --git a/framework/tests/accumulation/SimpleInferenceMerge.java b/framework/tests/accumulation/SimpleInferenceMerge.java new file mode 100644 index 00000000000..ffa3f0a56ab --- /dev/null +++ b/framework/tests/accumulation/SimpleInferenceMerge.java @@ -0,0 +1,37 @@ +import testaccumulation.qual.*; + +class SimpleInferenceMerge { + void build(@TestAccumulation({"a", "b"}) SimpleInferenceMerge this) {} + + void a() {} + + void b() {} + + void c() {} + + static void doStuffCorrectMerge(boolean b) { + SimpleInferenceMerge s = new SimpleInferenceMerge(); + if (b) { + s.a(); + s.b(); + } else { + s.b(); + s.a(); + s.c(); + } + s.build(); + } + + static void doStuffWrongMerge(boolean b) { + SimpleInferenceMerge s = new SimpleInferenceMerge(); + if (b) { + s.a(); + s.b(); + } else { + s.b(); + s.c(); + } + // :: error: method.invocation.invalid + s.build(); + } +} diff --git a/framework/tests/accumulation/SmallPredicate.java b/framework/tests/accumulation/SmallPredicate.java new file mode 100644 index 00000000000..9f463564fa9 --- /dev/null +++ b/framework/tests/accumulation/SmallPredicate.java @@ -0,0 +1,19 @@ +// small test case for predicates, for debugging + +import testaccumulation.qual.*; + +class SmallPredicate { + void a() {} + + void b() {} + + void d(@TestAccumulationPredicate("a && b") SmallPredicate this) {} + + static void test(SmallPredicate smallPredicate) { + smallPredicate.a(); + smallPredicate.b(); + @TestAccumulation({"a", "b"}) + SmallPredicate p2 = smallPredicate; + smallPredicate.d(); + } +} diff --git a/framework/tests/accumulation/UnparseablePredicate.java b/framework/tests/accumulation/UnparseablePredicate.java new file mode 100644 index 00000000000..01442c3a4de --- /dev/null +++ b/framework/tests/accumulation/UnparseablePredicate.java @@ -0,0 +1,50 @@ +import testaccumulation.qual.*; + +class UnparseablePredicate { + + // :: error: predicate.invalid + void unclosedOpen(@TestAccumulationPredicate("(foo && bar") Object x) {} + + // :: error: predicate.invalid + void unopenedClose(@TestAccumulationPredicate("foo || bar)") Object x) {} + + // :: error: predicate.invalid + void badKeywords1(@TestAccumulationPredicate("foo OR bar") Object x) {} + + // :: error: predicate.invalid + void badKeywords2(@TestAccumulationPredicate("foo AND bar") Object x) {} + + // These tests check that valid java identifiers don't cause problems + // when evaluating predicates. Examples of identifiers from + // https://docs.oracle.com/javase/specs/jls/se8/html/jls-3.html#jls-3.8 + + void jls0Example(@TestAccumulationPredicate("String") Object x) {} + + void callJls0Example(@TestAccumulation("String") Object y) { + jls0Example(y); + } + + void jls1Example(@TestAccumulationPredicate("i3") Object x) {} + + void callJls1Example(@TestAccumulation("i3") Object y) { + jls1Example(y); + } + + void jls2Example(@TestAccumulationPredicate("αρετη") Object x) {} + + void callJls2Example(@TestAccumulation("αρετη") Object y) { + jls2Example(y); + } + + void jls3Example(@TestAccumulationPredicate("MAX_VALUE") Object x) {} + + void callJls3Example(@TestAccumulation("MAX_VALUE") Object y) { + jls3Example(y); + } + + void jls4Example(@TestAccumulationPredicate("isLetterOrDigit") Object x) {} + + void callJls4Example(@TestAccumulation("isLetterOrDigit") Object y) { + jls4Example(y); + } +} diff --git a/framework/tests/accumulation/Xor.java b/framework/tests/accumulation/Xor.java new file mode 100644 index 00000000000..c92f6307a87 --- /dev/null +++ b/framework/tests/accumulation/Xor.java @@ -0,0 +1,64 @@ +import testaccumulation.qual.*; + +class Xor { + + class Foo { + void a() {} + + void b() {} + + void c() {} + // use a standard gate encoding for xor + void aXorB(@TestAccumulationPredicate("(a || b) && !(a && b)") Foo this) {} + } + + void test1(Foo f) { + // :: error: method.invocation.invalid + f.aXorB(); + } + + void test2(Foo f) { + f.c(); + // :: error: method.invocation.invalid + f.aXorB(); + } + + void test3(Foo f) { + f.a(); + f.aXorB(); + } + + void test4(Foo f) { + f.b(); + f.aXorB(); + } + + void test5(Foo f) { + f.a(); + f.b(); + // :: error: method.invocation.invalid + f.aXorB(); + } + + void callA(Foo f) { + f.a(); + } + + void test6(Foo f) { + callA(f); + f.b(); + // DEMONSTRATION OF "UNSOUNDNESS" + f.aXorB(); + } + + void test7(@TestAccumulation("a") Foo f) { + f.aXorB(); + } + + void test8(Foo f) { + callA(f); + // THIS IS AN UNAVOIDABLE FALSE POSITIVE + // :: error: method.invocation.invalid + f.aXorB(); + } +} diff --git a/framework/tests/aliasing/AliasingConstructorTest.java b/framework/tests/aliasing/AliasingConstructorTest.java new file mode 100644 index 00000000000..a8a0e7b29b3 --- /dev/null +++ b/framework/tests/aliasing/AliasingConstructorTest.java @@ -0,0 +1,23 @@ +import org.checkerframework.common.aliasing.qual.*; + +public class AliasingConstructorTest { + + public AliasingConstructorTest(@NonLeaked Object o) {} + + // int and String parameters on the constructors below are used only + // to make a distinction among constructors. + public AliasingConstructorTest(@LeakedToResult Object o, int i) {} + + public AliasingConstructorTest(Object o, String s) {} + + public void annosInAliasingConstructorTest() { + @Unique Object o = new Object(); + new AliasingConstructorTest(o); + Object o2 = new Object(); + new AliasingConstructorTest(o2, 1); + AliasingConstructorTest ct = new AliasingConstructorTest(o2, 1); + @Unique Object o3 = new Object(); + // ::error: (unique.leaked) + new AliasingConstructorTest(o3, "someString"); + } +} diff --git a/framework/tests/aliasing/ConstructorTest.java b/framework/tests/aliasing/ConstructorTest.java deleted file mode 100644 index 1a0fdb1c0e3..00000000000 --- a/framework/tests/aliasing/ConstructorTest.java +++ /dev/null @@ -1,23 +0,0 @@ -import org.checkerframework.common.aliasing.qual.*; - -public class ConstructorTest { - - public ConstructorTest(@NonLeaked Object o) {} - - // int and String parameters on the constructors below are used only - // to make a distinction among constructors. - public ConstructorTest(@LeakedToResult Object o, int i) {} - - public ConstructorTest(Object o, String s) {} - - public void annosInConstructorTest() { - @Unique Object o = new Object(); - new ConstructorTest(o); - Object o2 = new Object(); - new ConstructorTest(o2, 1); - ConstructorTest ct = new ConstructorTest(o2, 1); - @Unique Object o3 = new Object(); - // ::error: (unique.leaked) - new ConstructorTest(o3, "someString"); - } -} diff --git a/framework/tests/aliasing/ExplicitAnnotationTest.java b/framework/tests/aliasing/ExplicitAnnotationTest.java new file mode 100644 index 00000000000..dac83b177d1 --- /dev/null +++ b/framework/tests/aliasing/ExplicitAnnotationTest.java @@ -0,0 +1,15 @@ +import org.checkerframework.common.aliasing.qual.Unique; + +@Unique class Data { + @SuppressWarnings("unique.leaked") + Data() {} // All objects of Data are now @Unique +} + +class Demo { + void check(Data p) { // p is @Unique Data Object + // :: error: (unique.leaked) + Data y = p; // @Unique p is leaked + // :: error: (unique.leaked) + Object z = p; // @Unique p is leaked + } +} diff --git a/framework/tests/aliasing/UniqueAnnotation.java b/framework/tests/aliasing/UniqueAnnoTest.java similarity index 100% rename from framework/tests/aliasing/UniqueAnnotation.java rename to framework/tests/aliasing/UniqueAnnoTest.java diff --git a/framework/tests/aliasing/stubfile.astub b/framework/tests/aliasing/stubfile.astub deleted file mode 100644 index cb5a0b93333..00000000000 --- a/framework/tests/aliasing/stubfile.astub +++ /dev/null @@ -1,18 +0,0 @@ -import org.checkerframework.common.aliasing.qual.*; -package java.lang; -class String { - @Unique String(); -} - -class StringBuffer { - @Unique StringBuffer(); - StringBuffer append(@LeakedToResult StringBuffer this, @NonLeaked String s); -} - -class Exception { - @Unique Exception(); -} - -class Object { - @Unique Object(); -} diff --git a/framework/tests/all-systems/GetClassTest.java b/framework/tests/all-systems/GetClassTest.java index 118eecbdc56..8fdfa3746f6 100644 --- a/framework/tests/all-systems/GetClassTest.java +++ b/framework/tests/all-systems/GetClassTest.java @@ -11,8 +11,10 @@ void context() { // Type arguments don't match @SuppressWarnings("fenum:assignment.type.incompatible") Class b = i.getClass(); - // Type arguments don't match - @SuppressWarnings("fenum:assignment.type.incompatible") + @SuppressWarnings({ + "fenum:assignment.type.incompatible", // Type arguments don't match + "signedness:assignment.type.incompatible" // Type arguments don't match + }) Class c = i.getClass(); Class d = i.getClass(); diff --git a/framework/tests/all-systems/Issue457.java b/framework/tests/all-systems/Issue457.java index e1a55ebfa7b..0d825135658 100644 --- a/framework/tests/all-systems/Issue457.java +++ b/framework/tests/all-systems/Issue457.java @@ -6,6 +6,7 @@ class Issue457 { public void f(T t) { final T obj = t; + @SuppressWarnings("signedness:assignment.type.incompatible") // cast Float objFloat = (obj instanceof Float) ? (Float) obj : null; // An error will be emitted on this line before the fix for Issue457 diff --git a/framework/tests/annotationclassloader/Makefile b/framework/tests/annotationclassloader/Makefile index 56fa693f9cf..1b84c404286 100644 --- a/framework/tests/annotationclassloader/Makefile +++ b/framework/tests/annotationclassloader/Makefile @@ -37,7 +37,6 @@ demo1: -processor org.checkerframework.common.aliasing.AliasingChecker \ -Anomsgtext \ -ApermitMissingJdk \ - -Astubs=$(PROJECTDIR)/../aliasing/stubfile.astub \ LoaderTest.java # loads from framework.jar @@ -49,7 +48,6 @@ demo2: -processor org.checkerframework.common.aliasing.AliasingChecker \ -Anomsgtext \ -ApermitMissingJdk \ - -Astubs=$(PROJECTDIR)/../aliasing/stubfile.astub \ LoaderTest.java # ====================================================== @@ -61,7 +59,6 @@ load-from-dir-test: -classpath $(PROJECTDIR):${CHECKERQUALBUILD} \ -processor org.checkerframework.common.aliasing.AliasingChecker \ -Anomsgtext \ - -Astubs=$(PROJECTDIR)/../aliasing/stubfile.astub \ -ApermitMissingJdk \ LoaderTest.java > Out.txt 2>&1 || true diff -u Expected.txt Out.txt -I 'Note' @@ -75,7 +72,6 @@ load-from-jar-test: -processor org.checkerframework.common.aliasing.AliasingChecker \ -Anomsgtext \ -ApermitMissingJdk \ - -Astubs=$(PROJECTDIR)/../aliasing/stubfile.astub \ LoaderTest.java > Out.txt 2>&1 || true diff -u Expected.txt Out.txt rm -f Out.txt diff --git a/framework/tests/framework/AnnotatedAnnotation.java b/framework/tests/framework/AnnotatedAnnotation.java index 7874e4410d9..25387b4b337 100644 --- a/framework/tests/framework/AnnotatedAnnotation.java +++ b/framework/tests/framework/AnnotatedAnnotation.java @@ -17,10 +17,10 @@ } class Const { - @SuppressWarnings("test") + @SuppressWarnings("evenodd") public static final @Odd int ok1 = 5; - @SuppressWarnings("test") + @SuppressWarnings("evenodd") public static final @Odd int ok2 = 5; public static final int notodd = 4; diff --git a/framework/tests/framework/AnnotatedGenerics.java b/framework/tests/framework/AnnotatedGenerics.java index 48ac6b8afff..d449504ca36 100644 --- a/framework/tests/framework/AnnotatedGenerics.java +++ b/framework/tests/framework/AnnotatedGenerics.java @@ -100,19 +100,19 @@ public void testAnonymousConstructors() { // not seem to support the diamond operator in conjunction with anonymous classes. // // public void testAnonymousConstructorsWithTypeParameterInferrence() { - // @Odd MyClass<@Odd String> innerClass1 = new @Odd MyClass<>() {}; - // @Odd NormalClass<@Odd String> normal1 = new @Odd NormalClass<>() {}; + // @Odd MyClass<@Odd String> innerClass1 = new @Odd MyClass<>() {}; + // @Odd NormalClass<@Odd String> normal1 = new @Odd NormalClass<>() {}; // - // // Should error because the RHS isn't annotated as '@Odd' - // @Odd MyClass<@Odd String> innerClass2 = new MyClass<>() {}; - // @Odd NormalClass<@Odd String> normal2 = new NormalClass<>() {}; + // // Should error because the RHS isn't annotated as '@Odd' + // @Odd MyClass<@Odd String> innerClass2 = new MyClass<>() {}; + // @Odd NormalClass<@Odd String> normal2 = new NormalClass<>() {}; // - // @Odd MyClass innerClass3 = new @Odd MyClass<>() {}; - // @Odd NormalClass normal3 = new @Odd NormalClass<>() {}; + // @Odd MyClass innerClass3 = new @Odd MyClass<>() {}; + // @Odd NormalClass normal3 = new @Odd NormalClass<>() {}; // - // // Should error because the RHS isn't annotated as '@Odd' - // @Odd MyClass innerClass4 = new MyClass<>() {}; - // @Odd NormalClass normal4 = new NormalClass<>() {}; + // // Should error because the RHS isn't annotated as '@Odd' + // @Odd MyClass innerClass4 = new MyClass<>() {}; + // @Odd NormalClass normal4 = new NormalClass<>() {}; // } static class NormalClass { diff --git a/framework/tests/framework/README b/framework/tests/framework/README index e173ed32299..3e9f4e5e622 100644 --- a/framework/tests/framework/README +++ b/framework/tests/framework/README @@ -8,4 +8,4 @@ To run the tests, do To run a single test, do something like: cd $CHECKERFRAMEWORK/framework/tests/framework - (cd $CHECKERFRAMEWORK && ./gradle assemble :framework:compileTestJava) && javacheck -processor testlib.util.TestChecker -cp $CHECKERFRAMEWORK/framework/build/classes/java/test/ + (cd $CHECKERFRAMEWORK && ./gradle assemble :framework:compileTestJava) && javacheck -processor testlib.util.H1H2Checker -cp $CHECKERFRAMEWORK/framework/build/classes/java/test/ diff --git a/framework/tests/testchecker/AnonymousClasses.java b/framework/tests/h1h2checker/AnonymousClasses.java similarity index 91% rename from framework/tests/testchecker/AnonymousClasses.java rename to framework/tests/h1h2checker/AnonymousClasses.java index 74316c8970d..5646e4480f6 100644 --- a/framework/tests/testchecker/AnonymousClasses.java +++ b/framework/tests/h1h2checker/AnonymousClasses.java @@ -1,6 +1,6 @@ +import h1h2checker.quals.H1S1; +import h1h2checker.quals.H1S2; import java.util.Comparator; -import testchecker.quals.H1S1; -import testchecker.quals.H1S2; class AnonymousClasses { private <@H1S1 T extends @H1S1 Comparator> void testGenericAnonymous() { diff --git a/framework/tests/testchecker/Catch.java b/framework/tests/h1h2checker/Catch.java similarity index 97% rename from framework/tests/testchecker/Catch.java rename to framework/tests/h1h2checker/Catch.java index 66169af555d..6bc9e77298e 100644 --- a/framework/tests/testchecker/Catch.java +++ b/framework/tests/h1h2checker/Catch.java @@ -1,4 +1,4 @@ -import testchecker.quals.*; +import h1h2checker.quals.*; class Catch { void defaultUnionType() throws Throwable { diff --git a/framework/tests/testchecker/CompoundStringAssignment.java b/framework/tests/h1h2checker/CompoundStringAssignment.java similarity index 97% rename from framework/tests/testchecker/CompoundStringAssignment.java rename to framework/tests/h1h2checker/CompoundStringAssignment.java index 8eaa4f0fa56..b61af46d522 100644 --- a/framework/tests/testchecker/CompoundStringAssignment.java +++ b/framework/tests/h1h2checker/CompoundStringAssignment.java @@ -1,4 +1,4 @@ -import testchecker.quals.*; +import h1h2checker.quals.*; class CompoundStringAssignment { @H1S1 @H2S1 String getSib1() { diff --git a/framework/tests/testchecker/Constructors.java b/framework/tests/h1h2checker/Constructors.java similarity index 99% rename from framework/tests/testchecker/Constructors.java rename to framework/tests/h1h2checker/Constructors.java index f6011fac378..b5f0144a86c 100644 --- a/framework/tests/testchecker/Constructors.java +++ b/framework/tests/h1h2checker/Constructors.java @@ -1,4 +1,4 @@ -import testchecker.quals.*; +import h1h2checker.quals.*; class Constructors { // :: error: (super.invocation.invalid) :: warning: (inconsistent.constructor.type) diff --git a/framework/tests/testchecker/Defaulting.java b/framework/tests/h1h2checker/Defaulting.java similarity index 99% rename from framework/tests/testchecker/Defaulting.java rename to framework/tests/h1h2checker/Defaulting.java index 354f56239cd..7c26db3ae9d 100644 --- a/framework/tests/testchecker/Defaulting.java +++ b/framework/tests/h1h2checker/Defaulting.java @@ -1,6 +1,6 @@ +import h1h2checker.quals.*; import org.checkerframework.framework.qual.DefaultQualifier; import org.checkerframework.framework.qual.TypeUseLocation; -import testchecker.quals.*; // Test defaulting behavior, e.g. that local variables, casts, and instanceof // propagate the type of the respective sub-expression and that upper bounds diff --git a/framework/tests/testchecker/ForEach.java b/framework/tests/h1h2checker/ForEach.java similarity index 98% rename from framework/tests/testchecker/ForEach.java rename to framework/tests/h1h2checker/ForEach.java index c1963ea8edd..6544046f6ae 100644 --- a/framework/tests/testchecker/ForEach.java +++ b/framework/tests/h1h2checker/ForEach.java @@ -1,4 +1,4 @@ -import testchecker.quals.*; +import h1h2checker.quals.*; class ForEach { diff --git a/framework/tests/testchecker/Generics.java b/framework/tests/h1h2checker/Generics.java similarity index 97% rename from framework/tests/testchecker/Generics.java rename to framework/tests/h1h2checker/Generics.java index 2bdf7577424..9f5badf8056 100644 --- a/framework/tests/testchecker/Generics.java +++ b/framework/tests/h1h2checker/Generics.java @@ -1,4 +1,4 @@ -import testchecker.quals.*; +import h1h2checker.quals.*; class Generics { diff --git a/framework/tests/testchecker/GetClassStubTest.java b/framework/tests/h1h2checker/GetClassStubTest.java similarity index 91% rename from framework/tests/testchecker/GetClassStubTest.java rename to framework/tests/h1h2checker/GetClassStubTest.java index ae16f1a6187..12beb8f8555 100644 --- a/framework/tests/testchecker/GetClassStubTest.java +++ b/framework/tests/h1h2checker/GetClassStubTest.java @@ -1,6 +1,6 @@ -package testchecker; +package h1h2checker; -import testchecker.quals.*; +import h1h2checker.quals.*; class GetClassStubTest { diff --git a/framework/tests/testchecker/IncompatibleBounds.java b/framework/tests/h1h2checker/IncompatibleBounds.java similarity index 97% rename from framework/tests/testchecker/IncompatibleBounds.java rename to framework/tests/h1h2checker/IncompatibleBounds.java index d33a492bc43..0e2a1ba6f01 100644 --- a/framework/tests/testchecker/IncompatibleBounds.java +++ b/framework/tests/h1h2checker/IncompatibleBounds.java @@ -1,8 +1,8 @@ -package testchecker; +package h1h2checker; +import h1h2checker.quals.*; import org.checkerframework.framework.qual.DefaultQualifier; import org.checkerframework.framework.qual.TypeUseLocation; -import testchecker.quals.*; /** * This test is solely to ensure that if bounds in type parameters and wildcards are invalid then diff --git a/framework/tests/testchecker/InferTypeArgsPolyChecker.java b/framework/tests/h1h2checker/InferTypeArgsPolyChecker.java similarity index 99% rename from framework/tests/testchecker/InferTypeArgsPolyChecker.java rename to framework/tests/h1h2checker/InferTypeArgsPolyChecker.java index 7136f894797..4349537f92e 100644 --- a/framework/tests/testchecker/InferTypeArgsPolyChecker.java +++ b/framework/tests/h1h2checker/InferTypeArgsPolyChecker.java @@ -1,7 +1,7 @@ +import h1h2checker.quals.*; import java.util.ArrayList; import java.util.List; import java.util.Map; -import testchecker.quals.*; class InferTypeArgsPolyChecker { // ---------------------------------------------------------- diff --git a/framework/tests/testchecker/Inheritance.java b/framework/tests/h1h2checker/Inheritance.java similarity index 94% rename from framework/tests/testchecker/Inheritance.java rename to framework/tests/h1h2checker/Inheritance.java index d0bebed45ee..e342ce96df7 100644 --- a/framework/tests/testchecker/Inheritance.java +++ b/framework/tests/h1h2checker/Inheritance.java @@ -1,4 +1,4 @@ -import testchecker.quals.*; +import h1h2checker.quals.*; // :: warning: (inconsistent.constructor.type) :: error: (super.invocation.invalid) @H1S1 class Inheritance { diff --git a/framework/tests/testchecker/Issue681.java b/framework/tests/h1h2checker/Issue681.java similarity index 90% rename from framework/tests/testchecker/Issue681.java rename to framework/tests/h1h2checker/Issue681.java index 9aa2041e924..6c305846878 100644 --- a/framework/tests/testchecker/Issue681.java +++ b/framework/tests/h1h2checker/Issue681.java @@ -1,7 +1,7 @@ // Test case for Issue 681: // https://github.com/typetools/checker-framework/issues/681 -import testchecker.quals.H1S2; +import h1h2checker.quals.H1S2; // TODO: Issue is fixed, but test needs to be re-written in // a way that actually checks the behavior. @@ -14,7 +14,7 @@ * whose name contains "addH1S2". * *

        - * javacheck -cp tests/build/ -processor testchecker.TestChecker tests/testchecker/Issue681.java
        + * javacheck -cp tests/build/ -processor h1h2checker.H1H2Checker tests/h1h2checker/Issue681.java
          * javap -verbose Issue681\$Inner.class
          * 
          *
        diff --git a/framework/tests/testchecker/Issue798.java b/framework/tests/h1h2checker/Issue798.java
        similarity index 94%
        rename from framework/tests/testchecker/Issue798.java
        rename to framework/tests/h1h2checker/Issue798.java
        index b465124d761..5c798e6806a 100644
        --- a/framework/tests/testchecker/Issue798.java
        +++ b/framework/tests/h1h2checker/Issue798.java
        @@ -1,7 +1,7 @@
         // Test case for Issue 798
         // https://github.com/typetools/checker-framework/issues/798
         
        -import testchecker.quals.*;
        +import h1h2checker.quals.*;
         
         public class Issue798 {
             void test1(String format, @H1S1 Object @H1S2 ... args) {
        diff --git a/framework/tests/testchecker/Issue849.java b/framework/tests/h1h2checker/Issue849.java
        similarity index 83%
        rename from framework/tests/testchecker/Issue849.java
        rename to framework/tests/h1h2checker/Issue849.java
        index 54453b49dfa..c4b5e8bc52c 100644
        --- a/framework/tests/testchecker/Issue849.java
        +++ b/framework/tests/h1h2checker/Issue849.java
        @@ -1,8 +1,8 @@
         // Test case for Issue 849:
         // https://github.com/typetools/checker-framework/issues/849
         
        -import testchecker.quals.H1S2;
        -import testchecker.quals.H1Top;
        +import h1h2checker.quals.H1S2;
        +import h1h2checker.quals.H1Top;
         
         class Issue849 {
             class Gen {}
        diff --git a/framework/tests/testchecker/Primitive.java b/framework/tests/h1h2checker/Primitive.java
        similarity index 92%
        rename from framework/tests/testchecker/Primitive.java
        rename to framework/tests/h1h2checker/Primitive.java
        index 96171357139..95c717b934e 100644
        --- a/framework/tests/testchecker/Primitive.java
        +++ b/framework/tests/h1h2checker/Primitive.java
        @@ -1,4 +1,4 @@
        -import testchecker.quals.*;
        +import h1h2checker.quals.*;
         
         class Primitive {
             @SuppressWarnings("type.incompatible")
        diff --git a/framework/tests/testchecker/TypeRefinement.java b/framework/tests/h1h2checker/TypeRefinement.java
        similarity index 56%
        rename from framework/tests/testchecker/TypeRefinement.java
        rename to framework/tests/h1h2checker/TypeRefinement.java
        index 9dedd882cab..1004ddc7527 100644
        --- a/framework/tests/testchecker/TypeRefinement.java
        +++ b/framework/tests/h1h2checker/TypeRefinement.java
        @@ -1,17 +1,17 @@
        -import testchecker.quals.*;
        -import testchecker.quals.H1Invalid;
        +import h1h2checker.quals.*;
        +import h1h2checker.quals.H1Invalid;
         
         class TypeRefinement {
             // :: warning: (cast.unsafe.constructor.invocation)
             @H1Top Object o = new @H1S1 Object();
        -    // :: error: (testchecker.h1invalid.forbidden) :: warning: (cast.unsafe.constructor.invocation)
        +    // :: error: (h1h2checker.h1invalid.forbidden) :: warning: (cast.unsafe.constructor.invocation)
             @H1Top Object o2 = new @H1Invalid Object();
        -    // :: error: (testchecker.h1invalid.forbidden)
        +    // :: error: (h1h2checker.h1invalid.forbidden)
             @H1Top Object o3 = getH1Invalid();
         
        -    // :: error: (testchecker.h1invalid.forbidden)
        +    // :: error: (h1h2checker.h1invalid.forbidden)
             @H1Invalid Object getH1Invalid() {
        -        // :: error: (testchecker.h1invalid.forbidden) :: warning:
        +        // :: error: (h1h2checker.h1invalid.forbidden) :: warning:
                 // (cast.unsafe.constructor.invocation)
                 return new @H1Invalid Object();
             }
        diff --git a/framework/tests/testchecker/testchecker.astub b/framework/tests/h1h2checker/h1h2checker.astub
        similarity index 91%
        rename from framework/tests/testchecker/testchecker.astub
        rename to framework/tests/h1h2checker/h1h2checker.astub
        index 45339a45f47..e4b2470fc9f 100644
        --- a/framework/tests/testchecker/testchecker.astub
        +++ b/framework/tests/h1h2checker/h1h2checker.astub
        @@ -1,6 +1,6 @@
         package java.lang;
         
        -import testchecker.quals.*;
        +import h1h2checker.quals.*;
         
         public final class Object {
             public Class<@H1S1 ? extends @H1S1 Object> getClass();
        diff --git a/framework/tests/nontopdefault/TestCasting.java b/framework/tests/nontopdefault/TestCasting.java
        new file mode 100644
        index 00000000000..845840c0cef
        --- /dev/null
        +++ b/framework/tests/nontopdefault/TestCasting.java
        @@ -0,0 +1,20 @@
        +import testlib.nontopdefault.qual.NTDMiddle;
        +
        +@SuppressWarnings("inconsistent.constructor.type") // Not the point of this test
        +public class TestCasting {
        +    void repro(@NTDMiddle long startTime) {
        +        try {
        +            System.out.println("Inside try");
        +            return;
        +        } catch (Exception ex) {
        +            long timeTaken = startTime;
        +            @NTDMiddle double dblTimeTaken = timeTaken;
        +
        +            throw new IllegalArgumentException();
        +        } finally {
        +            long timeTaken2 = startTime;
        +            // This assignment used to fail.
        +            @NTDMiddle double dblTimeTaken2 = timeTaken2;
        +        }
        +    }
        +}
        diff --git a/framework/tests/reflection/ConstructorTest.java b/framework/tests/reflection/ReflectionConstructorTest.java
        similarity index 79%
        rename from framework/tests/reflection/ConstructorTest.java
        rename to framework/tests/reflection/ReflectionConstructorTest.java
        index 29900801fdb..0cfd8499a3a 100644
        --- a/framework/tests/reflection/ConstructorTest.java
        +++ b/framework/tests/reflection/ReflectionConstructorTest.java
        @@ -3,19 +3,19 @@
         import testlib.reflection.qual.Sibling2;
         import testlib.reflection.qual.Top;
         
        -class ConstructorTest {
        +class ReflectionConstructorTest {
             @Sibling1 int sibling1;
             @Sibling2 int sibling2;
         
             // :: error: (super.invocation.invalid) :: warning: (inconsistent.constructor.type)
        -    public @Sibling1 ConstructorTest(@Sibling1 int a) {}
        +    public @Sibling1 ReflectionConstructorTest(@Sibling1 int a) {}
         
             // :: error: (super.invocation.invalid) :: warning: (inconsistent.constructor.type)
        -    public @Sibling2 ConstructorTest(@Sibling2 int a, @Sibling2 int b) {}
        +    public @Sibling2 ReflectionConstructorTest(@Sibling2 int a, @Sibling2 int b) {}
         
             public void pass1() {
                 try {
        -            Class c = Class.forName("ConstructorTest");
        +            Class c = Class.forName("ReflectionConstructorTest");
                     Constructor init = c.getConstructor(new Class[] {Integer.class});
                     @Sibling1 int i = sibling1;
                     @Sibling1 Object o = init.newInstance(i);
        @@ -25,7 +25,7 @@ public void pass1() {
         
             public void pass2() {
                 try {
        -            Class c = Class.forName("ConstructorTest");
        +            Class c = Class.forName("ReflectionConstructorTest");
                     Constructor init = c.getConstructor(new Class[] {Integer.class, Integer.class});
                     @Sibling2 int a = sibling2;
                     int b = a;
        @@ -36,7 +36,7 @@ public void pass2() {
         
             public void fail1() {
                 try {
        -            Class c = ConstructorTest.class;
        +            Class c = ReflectionConstructorTest.class;
                     Constructor init = c.getConstructor(new Class[] {Integer.class});
                     // :: error: (argument.type.incompatible)
                     Object o = init.newInstance(sibling2);
        @@ -46,7 +46,7 @@ public void fail1() {
         
             public void fail2() {
                 try {
        -            Class c = ConstructorTest.class;
        +            Class c = ReflectionConstructorTest.class;
                     Constructor init = c.getConstructor(new Class[] {Integer.class});
                     // :: error: (assignment.type.incompatible)
                     @Sibling1 Object o = init.newInstance(new Object[] {sibling2});
        @@ -56,7 +56,7 @@ public void fail2() {
         
             public void fail3() {
                 try {
        -            Class c = Class.forName("ConstructorTest");
        +            Class c = Class.forName("ReflectionConstructorTest");
                     Constructor init = c.getConstructor(new Class[] {Integer.class, Integer.class});
                     @Sibling2 int a = sibling2;
                     @Sibling1 int b = sibling1;
        diff --git a/framework/tests/subtyping/README b/framework/tests/subtyping/README
        index cdf06dfe1cd..1d4699bcd7e 100644
        --- a/framework/tests/subtyping/README
        +++ b/framework/tests/subtyping/README
        @@ -3,4 +3,3 @@ To add a new file to the test suite, see
         
         To run the tests, do
           (cd $CHECKERFRAMEWORK && ./gradlew SubtypingEncryptedTest)
        -
        diff --git a/framework/tests/whole-program-inference/non-annotated/ExpectedErrors.java b/framework/tests/whole-program-inference/non-annotated/ExpectedErrors.java
        index f26cd258a2f..09d6c5bb0a8 100644
        --- a/framework/tests/whole-program-inference/non-annotated/ExpectedErrors.java
        +++ b/framework/tests/whole-program-inference/non-annotated/ExpectedErrors.java
        @@ -234,4 +234,11 @@ void test() {
                     expectsSibling1(field2);
                 }
             }
        +
        +    class AssignParam {
        +        public void f(@WholeProgramInferenceBottom Object param) {
        +            // :: error: assignment.type.incompatible
        +            param = ((@Top Object) null);
        +        }
        +    }
         }
        diff --git a/framework/tests/whole-program-inference/non-annotated/LocalClassTest.java b/framework/tests/whole-program-inference/non-annotated/LocalClassTest.java
        new file mode 100644
        index 00000000000..018f5c75b0d
        --- /dev/null
        +++ b/framework/tests/whole-program-inference/non-annotated/LocalClassTest.java
        @@ -0,0 +1,11 @@
        +// test case for https://github.com/typetools/checker-framework/issues/3461
        +
        +import testlib.wholeprograminference.qual.Sibling1;
        +
        +public class LocalClassTest {
        +    public void method() {
        +        class Local {
        +            Object o = (@Sibling1 Object) null;
        +        }
        +    }
        +}
        diff --git a/framework/tests/whole-program-inference/non-annotated/MultidimensionalAnnotatedArray.java b/framework/tests/whole-program-inference/non-annotated/MultidimensionalAnnotatedArray.java
        new file mode 100644
        index 00000000000..498751cee3d
        --- /dev/null
        +++ b/framework/tests/whole-program-inference/non-annotated/MultidimensionalAnnotatedArray.java
        @@ -0,0 +1,11 @@
        +// test case for https://github.com/typetools/checker-framework/issues/3422
        +
        +import testlib.wholeprograminference.qual.Sibling1;
        +
        +public class MultidimensionalAnnotatedArray {
        +    boolean[][] field = getArray();
        +
        +    public boolean[] @Sibling1 [] getArray() {
        +        return null;
        +    }
        +}
        diff --git a/framework/tests/whole-program-inference/non-annotated/OuterClassWithTypeParam.java b/framework/tests/whole-program-inference/non-annotated/OuterClassWithTypeParam.java
        new file mode 100644
        index 00000000000..3f02156b9c8
        --- /dev/null
        +++ b/framework/tests/whole-program-inference/non-annotated/OuterClassWithTypeParam.java
        @@ -0,0 +1,9 @@
        +// test file for https://github.com/typetools/checker-framework/issues/3438
        +
        +import testlib.wholeprograminference.qual.Sibling1;
        +
        +public class OuterClassWithTypeParam {
        +    public class InnerClass {
        +        Object o = (@Sibling1 Object) null;
        +    }
        +}
        diff --git a/framework/tests/whole-program-inference/non-annotated/Tempvars.java b/framework/tests/whole-program-inference/non-annotated/Tempvars.java
        new file mode 100644
        index 00000000000..a46b67df0c0
        --- /dev/null
        +++ b/framework/tests/whole-program-inference/non-annotated/Tempvars.java
        @@ -0,0 +1,8 @@
        +// test case for https://github.com/typetools/checker-framework/issues/3442
        +
        +public class Tempvars {
        +    static {
        +        int i = 0;
        +        i++;
        +    }
        +}
        diff --git a/javacutil/build.gradle b/javacutil/build.gradle
        index b1c19285f2a..eda180c68a5 100644
        --- a/javacutil/build.gradle
        +++ b/javacutil/build.gradle
        @@ -6,7 +6,7 @@ dependencies {
             // This is used by org.checkerframework.javacutil.TypesUtils.isImmutableTypeInJdk.
             // https://mvnrepository.com/artifact/org.plumelib/plume-util
             // Keep version number in sync with docs/developer/release/maven-artifacts/javacutil-pom.xml.
        -    implementation 'org.plumelib:plume-util:1.1.4'
        +    implementation 'org.plumelib:plume-util:1.1.5'
         
             implementation project(':checker-qual')
         }
        diff --git a/javacutil/src/main/java/org/checkerframework/javacutil/AnnotationUtils.java b/javacutil/src/main/java/org/checkerframework/javacutil/AnnotationUtils.java
        index 6164f1d821c..ae0e8c2505d 100644
        --- a/javacutil/src/main/java/org/checkerframework/javacutil/AnnotationUtils.java
        +++ b/javacutil/src/main/java/org/checkerframework/javacutil/AnnotationUtils.java
        @@ -32,6 +32,8 @@
         import javax.lang.model.element.TypeElement;
         import javax.lang.model.type.DeclaredType;
         import javax.lang.model.util.ElementFilter;
        +import org.checkerframework.checker.interning.qual.CompareToMethod;
        +import org.checkerframework.checker.interning.qual.EqualsMethod;
         import org.checkerframework.checker.nullness.qual.NonNull;
         import org.checkerframework.checker.nullness.qual.Nullable;
         import org.checkerframework.checker.signature.qual.DotSeparatedIdentifiers;
        @@ -78,6 +80,7 @@ public static final String annotationName(AnnotationMirror annotation) {
              * @param a2 the second AnnotationMirror to compare
              * @return true iff a1 and a2 are the same annotation
              */
        +    @EqualsMethod
             public static boolean areSame(AnnotationMirror a1, AnnotationMirror a2) {
                 if (a1 == a2) {
                     return true;
        @@ -105,8 +108,8 @@ public static boolean areSame(AnnotationMirror a1, AnnotationMirror a2) {
              * @param a2 the second AnnotationMirror to compare
              * @return true iff a1 and a2 have the same annotation name
              * @see #areSame(AnnotationMirror, AnnotationMirror)
        -     * @return true iff a1 and a2 have the same annotation name
              */
        +    @EqualsMethod
             public static boolean areSameByName(AnnotationMirror a1, AnnotationMirror a2) {
                 if (a1 == a2) {
                     return true;
        @@ -358,6 +361,7 @@ public static int compareAnnotationMirrors(AnnotationMirror a1, AnnotationMirror
              * @param av2 the second AnnotationValue to compare
              * @return 0 if if the two annotation values are the same
              */
        +    @CompareToMethod
             private static int compareAnnotationValue(AnnotationValue av1, AnnotationValue av2) {
                 if (av1 == av2) {
                     return 0;
        @@ -370,11 +374,14 @@ private static int compareAnnotationValue(AnnotationValue av1, AnnotationValue a
             }
         
             /**
        -     * Return 0 if the two annotation values are the same.
        +     * Compares two annotation values for order.
              *
              * @param val1 a value returned by {@code AnnotationValue.getValue()}
              * @param val2 a value returned by {@code AnnotationValue.getValue()}
        +     * @return a negative integer, zero, or a positive integer as the first annotation value is less
        +     *     than, equal to, or greater than the second annotation value
              */
        +    @CompareToMethod
             private static int compareAnnotationValueValue(@Nullable Object val1, @Nullable Object val2) {
                 if (val1 == val2) {
                     return 0;
        @@ -565,6 +572,7 @@ public static EnumSet getElementKindsForElementType(ElementType ele
              * @param am2 the second AnnotationMirror to compare
              * @return true if if the two annotations have the same elements (fields)
              */
        +    @EqualsMethod
             public static boolean sameElementValues(AnnotationMirror am1, AnnotationMirror am2) {
                 if (am1 == am2) {
                     return true;
        diff --git a/javacutil/src/main/java/org/checkerframework/javacutil/ElementUtils.java b/javacutil/src/main/java/org/checkerframework/javacutil/ElementUtils.java
        index 30e4959bb51..3cc635da736 100644
        --- a/javacutil/src/main/java/org/checkerframework/javacutil/ElementUtils.java
        +++ b/javacutil/src/main/java/org/checkerframework/javacutil/ElementUtils.java
        @@ -21,11 +21,9 @@
         import javax.lang.model.element.PackageElement;
         import javax.lang.model.element.TypeElement;
         import javax.lang.model.element.VariableElement;
        -import javax.lang.model.type.ArrayType;
         import javax.lang.model.type.DeclaredType;
         import javax.lang.model.type.TypeKind;
         import javax.lang.model.type.TypeMirror;
        -import javax.lang.model.type.TypeVariable;
         import javax.lang.model.util.ElementFilter;
         import javax.lang.model.util.Elements;
         import javax.tools.JavaFileObject;
        @@ -199,7 +197,7 @@ public static String getSimpleName(ExecutableElement element) {
                 sb.append("(");
                 for (Iterator i = element.getParameters().iterator();
                         i.hasNext(); ) {
        -            sb.append(simpleTypeName(i.next().asType()));
        +            sb.append(TypesUtils.simpleTypeName(i.next().asType()));
                     if (i.hasNext()) {
                         sb.append(",");
                     }
        @@ -209,28 +207,6 @@ public static String getSimpleName(ExecutableElement element) {
                 return sb.toString();
             }
         
        -    /**
        -     * Returns the simple type name, without annotations.
        -     *
        -     * @param type a type
        -     * @return the simple type name, without annotations
        -     */
        -    private static String simpleTypeName(TypeMirror type) {
        -        switch (type.getKind()) {
        -            case ARRAY:
        -                return simpleTypeName(((ArrayType) type).getComponentType()) + "[]";
        -            case TYPEVAR:
        -                return ((TypeVariable) type).asElement().getSimpleName().toString();
        -            case DECLARED:
        -                return ((DeclaredType) type).asElement().getSimpleName().toString();
        -            default:
        -                if (type.getKind().isPrimitive()) {
        -                    return TypeAnnotationUtils.unannotatedType(type).toString();
        -                }
        -        }
        -        throw new BugInCF("ElementUtils: unhandled type kind: %s, type: %s", type.getKind(), type);
        -    }
        -
             /**
              * Check if the element is an element for 'java.lang.Object'
              *
        diff --git a/javacutil/src/main/java/org/checkerframework/javacutil/SystemUtil.java b/javacutil/src/main/java/org/checkerframework/javacutil/SystemUtil.java
        index b0bf27b9558..8c3e41e6b6b 100644
        --- a/javacutil/src/main/java/org/checkerframework/javacutil/SystemUtil.java
        +++ b/javacutil/src/main/java/org/checkerframework/javacutil/SystemUtil.java
        @@ -9,6 +9,7 @@
         import java.io.FileReader;
         import java.io.IOException;
         import java.util.ArrayList;
        +import java.util.Arrays;
         import java.util.List;
         import java.util.regex.Matcher;
         import java.util.regex.Pattern;
        @@ -225,4 +226,35 @@ public static int getJreVersion() {
                 }
                 return javaHome + File.separator + "lib" + File.separator + "tools.jar";
             }
        +
        +    /**
        +     * Concatenates two arrays. Can be invoked varargs-style.
        +     *
        +     * @param  the type of the array elements
        +     * @param array1 the first array
        +     * @param array2 the second array
        +     * @return a new array containing the contents of the given arrays, in order
        +     */
        +    @SuppressWarnings("unchecked")
        +    public static  T[] concatenate(T[] array1, T... array2) {
        +        @SuppressWarnings("nullness") // elements are not non-null yet, but will be by return stmt
        +        T[] result = Arrays.copyOf(array1, array1.length + array2.length);
        +        System.arraycopy(array2, 0, result, array1.length, array2.length);
        +        return result;
        +    }
        +
        +    /**
        +     * Like Thread.sleep, but does not throw any exceptions, so it is easier for clients to use.
        +     * Causes the currently executing thread to sleep (temporarily cease execution) for the
        +     * specified number of milliseconds.
        +     *
        +     * @param millis the length of time to sleep in milliseconds
        +     */
        +    public static void sleep(long millis) {
        +        try {
        +            Thread.sleep(millis);
        +        } catch (InterruptedException ex) {
        +            Thread.currentThread().interrupt();
        +        }
        +    }
         }
        diff --git a/javacutil/src/main/java/org/checkerframework/javacutil/TreeUtils.java b/javacutil/src/main/java/org/checkerframework/javacutil/TreeUtils.java
        index 5fd50be43ad..9093d746053 100644
        --- a/javacutil/src/main/java/org/checkerframework/javacutil/TreeUtils.java
        +++ b/javacutil/src/main/java/org/checkerframework/javacutil/TreeUtils.java
        @@ -68,6 +68,7 @@
         import javax.lang.model.type.TypeKind;
         import javax.lang.model.type.TypeMirror;
         import javax.lang.model.util.ElementFilter;
        +import org.checkerframework.checker.interning.qual.PolyInterned;
         import org.checkerframework.checker.nullness.qual.EnsuresNonNullIf;
         import org.checkerframework.checker.nullness.qual.NonNull;
         import org.checkerframework.checker.nullness.qual.Nullable;
        @@ -341,7 +342,9 @@ public static boolean isSelfAccess(final ExpressionTree tree) {
              * @param tree an expression tree
              * @return the outermost non-parenthesized tree enclosed by the given tree
              */
        -    public static ExpressionTree withoutParens(final ExpressionTree tree) {
        +    @SuppressWarnings("interning:return.type.incompatible") // polymorphism implementation
        +    public static @PolyInterned ExpressionTree withoutParens(
        +            final @PolyInterned ExpressionTree tree) {
                 ExpressionTree t = tree;
                 while (t.getKind() == Tree.Kind.PARENTHESIZED) {
                     t = ((ParenthesizedTree) t).getExpression();
        @@ -409,7 +412,9 @@ public static Pair enclosingNonParen(final TreePath path) {
                         return getAssignmentContext(parentPath);
                     case CONDITIONAL_EXPRESSION:
                         ConditionalExpressionTree cet = (ConditionalExpressionTree) parent;
        -                if (cet.getCondition() == treePath.getLeaf()) {
        +                @SuppressWarnings("interning:not.interned") // AST node comparison
        +                boolean conditionIsLeaf = (cet.getCondition() == treePath.getLeaf());
        +                if (conditionIsLeaf) {
                             // The assignment context for the condition is simply boolean.
                             // No point in going on.
                             return null;
        diff --git a/javacutil/src/main/java/org/checkerframework/javacutil/TypeAnnotationUtils.java b/javacutil/src/main/java/org/checkerframework/javacutil/TypeAnnotationUtils.java
        index f3c833f0614..793c945d8e2 100644
        --- a/javacutil/src/main/java/org/checkerframework/javacutil/TypeAnnotationUtils.java
        +++ b/javacutil/src/main/java/org/checkerframework/javacutil/TypeAnnotationUtils.java
        @@ -88,6 +88,7 @@ public static boolean isSameTAPosition(TypeAnnotationPosition p1, TypeAnnotation
              * @param p2 the second position
              * @return true, iff the two positions are equal except for the source tree position
              */
        +    @SuppressWarnings("interning:not.interned") // reference equality for onLambda field
             public static boolean isSameTAPositionExceptTreePos(
                     TypeAnnotationPosition p1, TypeAnnotationPosition p2) {
                 return p1.type == p2.type
        diff --git a/javacutil/src/main/java/org/checkerframework/javacutil/TypeSystemError.java b/javacutil/src/main/java/org/checkerframework/javacutil/TypeSystemError.java
        new file mode 100644
        index 00000000000..1fd3a52bb99
        --- /dev/null
        +++ b/javacutil/src/main/java/org/checkerframework/javacutil/TypeSystemError.java
        @@ -0,0 +1,32 @@
        +package org.checkerframework.javacutil;
        +
        +/**
        + * Exception type indicating a mistake by a type system built using the Checker Framework. For
        + * example, misusing a meta-annotation on a qualifier. These should only be thrown by the framework
        + * package.
        + */
        +@SuppressWarnings("serial")
        +public class TypeSystemError extends RuntimeException {
        +
        +    /**
        +     * Constructs a new TypeSystemError with the specified detail message.
        +     *
        +     * @param message the detail message
        +     */
        +    public TypeSystemError(String message) {
        +        super(message);
        +        if (message == null) {
        +            throw new Error("Must have a detail message.");
        +        }
        +    }
        +
        +    /**
        +     * Constructs a new TypeSystemError with a detail message composed from the given arguments.
        +     *
        +     * @param fmt the format string
        +     * @param args the arguments for the format string
        +     */
        +    public TypeSystemError(String fmt, Object... args) {
        +        this(String.format(fmt, args));
        +    }
        +}
        diff --git a/javacutil/src/main/java/org/checkerframework/javacutil/TypesUtils.java b/javacutil/src/main/java/org/checkerframework/javacutil/TypesUtils.java
        index 21a6b461ce6..42f7e1749a1 100644
        --- a/javacutil/src/main/java/org/checkerframework/javacutil/TypesUtils.java
        +++ b/javacutil/src/main/java/org/checkerframework/javacutil/TypesUtils.java
        @@ -58,7 +58,7 @@ public static boolean isObject(TypeMirror type) {
             }
         
             /**
        -     * Checks if the type represents a java.lang.Class declared type.
        +     * Checks if the type represents the java.lang.Class declared type.
              *
              * @param type the type
              * @return true iff type represents java.lang.Class
        @@ -180,12 +180,15 @@ public static boolean isPrimitive(TypeMirror type) {
             }
         
             /**
        -     * Returns true iff the arguments are both the same primitive types.
        +     * Returns true iff the arguments are both the same declared types.
              *
              * 

        This is needed because class {@code Type.ClassType} does not override equals. * - * @return whether the arguments are the same primitive types + * @param t1 the first type to test + * @param t2 the second type to test + * @return whether the arguments are the same declared types */ + @SuppressWarnings("interning:not.interned") // equality test optimization public static boolean areSameDeclaredTypes(Type.ClassType t1, Type.ClassType t2) { // Do a cheaper test first if (t1.tsym.name != t2.tsym.name) { @@ -245,9 +248,25 @@ public static boolean isIntegral(TypeMirror type) { } } + /** + * Returns true iff the argument is a boxed floating point type. + * + * @param type type to test + * @return whether the argument is a boxed floating point type + */ + public static boolean isBoxedFloating(TypeMirror type) { + if (type.getKind() != TypeKind.DECLARED) { + return false; + } + + String qualifiedName = getQualifiedName((DeclaredType) type).toString(); + return qualifiedName.equals("java.lang.Double") || qualifiedName.equals("java.lang.Float"); + } + /** * Returns true iff the argument is a floating point type. * + * @param type type mirror * @return whether the argument is a floating point type */ public static boolean isFloating(TypeMirror type) { @@ -680,4 +699,33 @@ public static boolean isFunctionalInterface(TypeMirror type, ProcessingEnvironme com.sun.tools.javac.code.Types javacTypes = com.sun.tools.javac.code.Types.instance(ctx); return javacTypes.isFunctionalInterface((Type) type); } + + /** + * Returns the simple type name, without annotations. + * + * @param type a type + * @return the simple type name, without annotations + */ + public static String simpleTypeName(TypeMirror type) { + switch (type.getKind()) { + case ARRAY: + return simpleTypeName(((ArrayType) type).getComponentType()) + "[]"; + case TYPEVAR: + return ((TypeVariable) type).asElement().getSimpleName().toString(); + case DECLARED: + return ((DeclaredType) type).asElement().getSimpleName().toString(); + case NULL: + return ""; + case VOID: + return "void"; + default: + if (type.getKind().isPrimitive()) { + return TypeAnnotationUtils.unannotatedType(type).toString(); + } else { + throw new BugInCF( + "simpleTypeName: unhandled type kind: %s, type: %s", + type.getKind(), type); + } + } + } }